【问题标题】:c++ virtual templated functionc++虚拟模板函数
【发布时间】:2017-06-10 18:16:09
【问题描述】:

可以理解,函数不能既是模板化的又是虚拟的。 但是可能有一种超级智能的设计模式可以做到。

我的目标是有一个看起来像这样的函数:

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;
}

如果这一切只是一个糟糕的开始,我应该考虑其他任何方法吗?我检查了“类型擦除”,但这似乎没有帮助(或者我错过了什么?)

【问题讨论】:

  • 您可能有一个受保护的非模板虚函数,它返回一个boost::any,然后将get 模板函数设为非虚函数并在那里执行转换?
  • 与问题完全无关......但是你为什么要传入 shared_ptr?
  • @TartanLlama 听起来很有希望......你有没有机会详细说明:)?
  • @rubenvb 指针的所有优点没有缺点...
  • @Vince 这句话总结了使用 shared_ptr 的所有错误原因。据我所知,这个函数没有参数的所有权。如果参数超过函数调用,只需通过 (const) ref 传递它。无需开销即可获得 C++ 的所有优点。

标签: c++ templates polymorphism virtual type-erasure


【解决方案1】:

看起来你有一些可能的类型,所以我建议将一组虚拟函数与非虚拟调度模板组合在一起:

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。这可以通过在 ConfigurationYAML_config 类之间插入一个 CRTP 来完成:它的作用是生成覆盖的函数。

注意: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

【讨论】:

  • 它不会消除使用模板的所有优点吗?在这种情况下,忘记模板并手动创建所有函数、虚函数和具体函数变得更容易、更直接。
  • @Vince 当您将模板层连接到其他通用代码时,模板层相对于普通虚拟函数的优势就会显现出来。 IOW,可以从T 上参数化的另一个模板调用cfg.get&lt;T&gt;("foo")。有关示例,请参阅 this recent answer of mine
  • @Vince 我已经在蛋糕上添加了另一层,所以你可以再次拥有你的模板;)
  • 基类的编译工作,但我没有设法创建我梦想的功能,编译给出错误:“无效使用模板名称'配置'没有参数列表”和“ISO C++禁止声明没有类型的“config””,指的是“void configure(const Configuration &config)”行
  • @Vince 使用ConfigurationBase,您不需要在用户代码中使用Configuration。也许您想将ConfigurationBase 重命名为Configuration,并将Configuration 重命名为ConfigurationBridge,以便与您现有的代码保持一致:)
【解决方案2】:

我已将my answer 改编为今天早些时候的一个类似问题,该问题使用类型擦除和 RTTI 来获得虚拟模板函数的效果。正如我在那里指出的,如果您不能或不想使用 RTTI,可以使用Boost.TypeIndex

基本实现如下所示(只需填写您的 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*。另外,在没有注册适当的getter的情况下返回一个默认值。您可能希望抛出异常,或返回 std::optional&lt;T&gt;std::pair&lt;T, bool&gt;,以将这些情况与实际映射到特定键的默认值区分开来。

此解决方案的优点是子类可以为任何类型注册 getter。但是,如果您知道 config::get&lt;T&gt; 需要使用的类型子集,肯定会有更有效的解决方案。

【讨论】:

    猜你喜欢
    • 2018-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-28
    • 2011-12-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多