C++ виртуальная шаблонная функция

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

Моя цель - иметь функцию, которая выглядит так:

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;

Вопрос: какой код подходит для класса Configuration?

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

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

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


person Vince    schedule 25.01.2017    source источник
comment
Возможно, у вас может быть защищенная виртуальная функция без шаблона, которая возвращает boost::any, а затем сделать функцию шаблона get невиртуальной и выполнить там приведение?   -  person TartanLlama    schedule 25.01.2017
comment
Совершенно не связанный с вопросом... Но почему вы передаете shared_ptr?   -  person rubenvb    schedule 25.01.2017
comment
@TartanLlama звучит многообещающе ... есть шанс, что вы уточните :)?   -  person Vince    schedule 25.01.2017
comment
@rubenvb все преимущества указателей без недостатков...   -  person Vince    schedule 25.01.2017
comment
@Vince И это предложение суммирует все неправильные причины использования shared_ptr. Эта функция, насколько я понимаю, не владеет параметром. Если параметр переживает вызов функции, просто передайте его (const) ref. Все преимущества C++ без накладных расходов.   -  person rubenvb    schedule 25.01.2017
comment
@rubenvb достаточно честно, я ответил быстро. Настоящая причина в том, что в моем контексте мне нужно, чтобы конфигурация находилась в статической памяти и создавалась в функции инициализации во время выполнения.   -  person Vince    schedule 25.01.2017
comment
Тем не менее, нет причин не использовать ссылку (на локальную статическую функцию, в случае, который вы описываете).   -  person rubenvb    schedule 25.01.2017
comment
Давайте продолжим это обсуждение в чате.   -  person Vince    schedule 25.01.2017
comment
@ Винс Я думаю, что ответ Квентина лучше, чем мое предложение. Если вы хотите просто выполнить одну функцию, вам нужно использовать среду выполнения в качестве моста, например: godbolt .org/g/9ZkuDe   -  person TartanLlama    schedule 25.01.2017


Ответы (2)


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

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");

Посмотрите на 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 {};
    }
};

Посмотрите на Coliru

person Quentin    schedule 25.01.2017
comment
разве это не устраняет все преимущества использования шаблона? В этом случае становится проще и понятнее забыть о шаблонах и создавать вручную все функции, виртуалы и конкретные. - person Vince; 25.01.2017
comment
@Vince преимущество уровня шаблона над простыми виртуальными функциями проявляется, когда вы подключаете его к другому универсальному коду. IOW можно вызвать cfg.get<T>("foo") из другого шаблона, параметризованного для T. См. этот мой недавний ответ для пример. - person Quentin; 25.01.2017
comment
@Vince Я добавил еще один слой в торт, так что вы снова можете получить свой шаблон;) - person Quentin; 25.01.2017
comment
компиляция базовых классов работает, но мне не удалось создать функцию моей мечты, компиляция выдает ошибку: недопустимое использование имени шаблона «Конфигурация» без списка аргументов, а ISO C++ запрещает объявление «конфигурации» без ссылки на тип строка void configure(const Configuration &config) - person Vince; 25.01.2017
comment
@Vince Используйте ConfigurationBase, вам не нужно использовать Configuration из пользовательского кода. Возможно, вы захотите переименовать ConfigurationBase в Configuration, а Configuration в ConfigurationBridge, чтобы это соответствовало вашему существующему коду :) - person Quentin; 25.01.2017
comment
Я обновил функцию, чтобы получить ссылку на экземпляр ConfigurationBase, и теперь компилятор жалуется, что ConfigurationBase не имеет члена с именем «get». Что-нибудь, что я могу пропустить? - person Vince; 25.01.2017
comment
@ Винс, ты позаботился заменить // ... фрагментом из первого фрагмента? - person Quentin; 25.01.2017
comment
Я должен быть честным, я ничего не понимаю. Я просто пытаюсь скомпилировать вещи в первую очередь и надеюсь, что скоро смогу разобраться в этом. Какой фрагмент из первого сниппета? - person Vince; 25.01.2017
comment
Давайте продолжим обсуждение в чате. - person Quentin; 25.01.2017

Я адаптировал свой ответ на аналогичный вопрос, заданный ранее сегодня, в котором используется стирание типов и 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>.

person Joseph Thomson    schedule 25.01.2017