2017-01-25 4 views
3

Понятно, что функции не могут быть как шаблонами, так и виртуальными. Но там может быть супер умный шаблон дизайна, который будет делать.C++ virtual templated function

Моя цель состоит в том, чтобы иметь функцию, которая выглядит следующим образом:

void configure(const Configuration &config){ 
    double stuff = config.get<double>("stuff"); 
    int thing = config.get<int>("thing"); 
    // rest of the code 
} 

В идеале, я мог бы передать различные объекты конфигурации, например, объект, который читается из файла или из базы данных.

Здесь (раздели до минимума) пример конкретного класса конфигурации с помощью YAML-CPP (я думаю, понятно, даже если вы не знаете, YAML-CPP):

class YAML_config : public Configuration { 
public: 
    YAML_config(std::string file_path){ 
    this->node = YAML::LoadFile(file_path); 
    } 
    template<typename T> T get(std::string key){ 
    return this->node[key].as<T>(); 
    } 
private: 
    YAML::Node node; 

Вопрос: что будет подходящий код для конфигурации класса?

Вот некоторые неверный код, который показывает намереваюсь:

class Configuration { 
    virtual template<typename T> T get(std::string key)=0; 
} 

Если все это просто плохое начало, любой другой подход, я должен смотреть в? Я проверил «стирание типа», но это, похоже, не помогло (или я что-то пропустил?)

+0

Возможно, у вас может быть защищенная виртуальная функция без шаблона, которая возвращает 'boost :: any', а затем делает функцию' get' template не виртуальной и выполняет листинг там? – TartanLlama

+0

Полностью не связан с вопросом .... Но почему вы проходите в shared_ptr? – rubenvb

+0

@TartanLlama звучит многообещающе ... любой шанс, который вы бы уточнили :)? – Vince

ответ

3

Похоже, у вас есть малый набор возможных типов, поэтому я предлагаю набор виртуальных функций, сгруппированных вместе с шаблоном невиртуальным диспетчерским:

template <class T> 
struct tag { }; 

class Configuration { 
public: 
    template <class T> 
    T get(std::string key) { 
     return get_(tag<T>{}, std::move(key)); 
    } 

protected: 
    virtual int get_(tag<int>, std::string key) = 0; 
    virtual double get_(tag<double>, std::string key) = 0; 
    virtual std::string get_(tag<std::string>, std::string key) = 0; 
}; 

class YAML_config : public Configuration { 
    int get_(tag<int>, std::string key) override { /* ... */ } 
    double get_(tag<double>, std::string key) override { /* ... */ } 
    std::string get_(tag<std::string>, std::string key) override { /* ... */ } 
}; 

Использование:

YAML_config cfg; 
auto s = cfg.get<int>("hello"); 

See it live on Coliru


Но мы потеряли способность объявлять YAML_config::get в качестве шаблона - типы в стороне, реализации все одинаковы, но мы не можем переопределить виртуальную функцию с помощью шаблона.

Итак, теперь, когда мы преодолели промежуток от шаблонов до виртуальных функций для достижения полиморфизма, давайте перейдем от виртуальных функций к шаблонам, чтобы вернуть наш хороший API. Это можно сделать путем прореживания в CRTP между классами Configuration и YAML_config: его роль будет заключаться в создании функций переопределения.

Примечание: виртуальные функции get_ теперь называются getBridge. Я добавил тире макросов, чтобы сократить повторение. Они могут быть дополнительно учтены с помощью Boost.PP, например.

class ConfigurationBase { 

// ... 

#define DECLARE_CONFIG_BRIDGE(T) \ 
    virtual T getBridge(tag<T>, std::string key) = 0; 

    DECLARE_CONFIG_BRIDGE(int) 
    DECLARE_CONFIG_BRIDGE(double) 
    DECLARE_CONFIG_BRIDGE(std::string) 

#undef DECLARE_CONFIG_BRIDGE 
}; 

template <class Derived> 
class Configuration : public ConfigurationBase { 

    // Hide ConfigurationBase::get so we don't get 
    // infinite recursion if we forget an implementation 
    // in the derived class. 
    template <class> 
    void get(...) = delete; 

#define OVERRIDE_CONFIG_BRIDGE(T) \ 
    T getBridge(tag<T>, std::string key) override { \ 
     return dThis()->template get<T>(std::move(key)); \ 
    } 

    OVERRIDE_CONFIG_BRIDGE(int) 
    OVERRIDE_CONFIG_BRIDGE(double) 
    OVERRIDE_CONFIG_BRIDGE(std::string) 

#undef OVERRIDE_CONFIG_BRIDGE 

    Derived *dThis() { 
     return static_cast<Derived*>(this); 
    } 
}; 

class YAML_config : public Configuration<YAML_config> { 
public: 
    template <class T> 
    T get(std::string) { 
     return {}; 
    } 
}; 

See it live on Coliru

+0

не устраняет ли все преимущества использования шаблона? В этом случае становится проще и проще забыть о шаблонах и вручную создавать все функции, виртуальные и бетонные блоки. – Vince

+0

@ Ввиду преимущества слоя шаблона поверх простых виртуальных функций появляется, когда вы подключаете его к другому универсальному коду. IOW, можно вызвать 'cfg.get (" foo ")' из другого шаблона, параметризованного на 'T'. См. [Этот последний мой ответ] (http://stackoverflow.com/questions/41806062/c-conditional-templates-compilation-based-on-data-type/41806521#41806521) для примера. – Quentin

+0

@Vince Я добавил еще один слой к пирогу, так что вы можете снова создать свой шаблон;) – Quentin

1

Я адаптировал my answer к аналогичному вопросу от ранее сегодня, который использует тип стирание и RTTI, чтобы получить эффект виртуальных шаблонных функций. Как я отметил там, Boost.TypeIndex может использоваться, если вы не можете или не хотите использовать RTTI.

Основная реализация выглядит примерно так (просто заполнить в вашей библиотеке вещи YAML):

#include <functional> 
#include <typeindex> 
#include <unordered_map> 

class config { 
public: 
    template <typename T> 
    T get(char const* key) { 
     T value = {}; 
     auto it = getters.find(type_index<T>()); 
     if (it != getters.end()) { 
      it->second(&value, key); 
     } 
     return value; 
    } 

protected: 
    template <typename T, typename Getter> 
    void register_getter(Getter getter) { 
     getters[type_index<T>()] = [getter](void* value, char const* key) { 
      *static_cast<T*>(value) = getter(key); 
     }; 
    } 

private: 
    template <typename T> 
    static std::type_index type_index() { 
     return std::type_index(typeid(std::remove_cv_t<T>)); 
    } 

    std::unordered_map<std::type_index, std::function<void (void*, char const*)>> getters; 
}; 

Использование будет выглядеть следующим образом (обратите внимание, что вы могли бы использовать композицию вместо наследования, если вы на самом деле не нужно config быть базовый класс):

#include <iostream> 

class yaml_config : public config { 
public: 
    yaml_config() { 
     register_getter<int>([](char const* key) { 
      return 42; 
     }); 
     register_getter<float>([](char const* key) { 
      return 3.14f; 
     }); 
    } 
}; 

int main() { 
    yaml_config cfg; 

    std::cout << cfg.get<int>("foo") << "\n"; 
    std::cout << cfg.get<float>("bar") << "\n"; 
    std::cout << cfg.get<short>("baz") << "\n"; 
} 

Выход:

42 
3.14 
0 

В этой конкретной реализации T должен быть по умолчанию конструктивным; если это неприемлемо, вы можете использовать std::any вместо void*. Кроме того, значение по умолчанию возвращается в случае, когда соответствующий получатель не зарегистрирован. Вы можете захотеть выбросить исключение или вернуть std::optional<T> или std::pair<T, bool>, чтобы отличить эти случаи от значения по умолчанию, которое фактически отображается на конкретный ключ.

Это решение имеет то преимущество, что подклассы могут регистрировать геттеры для любого типа. Тем не менее, есть, конечно, более эффективные решения, если вы знаете подмножество типов, с которыми необходимо работать config::get<T>.