【问题标题】:C++ keeping a collection of pointers to template objects, all derived from a non-template classC++ 保存指向模板对象的指针集合,所有这些都派生自非模板类
【发布时间】:2013-02-07 03:54:07
【问题描述】:

我有一个对象“标识符”列表(一个长枚举列表,每个“标识符”都有一个唯一值):

enum Identifier {
  Enum0,  // an identifier for a bool value
  Enum1,  //  ... for a float value
  Enum2,  //  ... for an int value
  // etc.
};

我希望维护一个与这些标识符相关联的 Value 对象的集合。这些 Value 对象包含单个值,但该值可能是整数、浮点数、布尔值或其他一些(简单)类型。这是在管理系统中的一组配置值的上下文中。稍后我计划扩展这些值类型以支持内部值的验证,并将一些值与其他值相关联。

但是我希望对这些值类使用模板,因为我想对这些值进行一般性的操作。如果我要使用继承,我将拥有 BaseValue,然后从 BaseValue 派生 IntValue、FloatValue 等。相反,我有价值、价值等。

但我还想在单个集合中存储对这些值中的每一个的访问机制。我希望一个类实例化所有这些并将它们维护在集合中。如果我使用继承,我可以使用指向 BaseValue 的指针向量。但是因为我使用的是模板,所以这些类之间并没有多态相关。

所以我考虑让它们基于一个参数化的(空的?)抽象基类:

class BaseParameter {
};

template<typename T>
class Parameter : public BaseParameter {
 public:
  explicit Parameter(T val) : val_(val) {}
  void set(ParameterSource src) { val_ = extract<T>(src); }
  T get() { return val_; };
 private:
  T val_;
};

请注意,“set”成员函数采用“ParameterSource”,它是由特定“to_type”函数“重新解释”的值的来源。这是我无法控制的 API 函数 - 我必须自己解释类型,因为我知道类型的含义,设置如下。这就是 extract 的作用——它专门用于各种 T 类型,例如 float、int、bool。

然后我可以像这样将它们添加到 std::vector 中:

std::vector<BaseParameter *> vec(10);
vec[Enum0] = new Parameter<bool>(true);   // this is where I state that it's a 'bool'
vec[Enum1] = new Parameter<float>(0.5);   //  ... or a float ...
vec[Enum2] = new Parameter<int>(42);      //  ... or an int ...

我知道我可能应该使用 unique_ptr 但现在我只是想让它工作。到目前为止,这似乎工作正常。但我对此持谨慎态度,因为我不确定实例化模板的完整类型是否会在运行时保留。

稍后我想通过任意枚举值索引“vec”,检索参数并在其上调用成员函数:

void set_via_source(Identifier id, ParameterSource source) {
  // if id is in range...
  vec[id]->set(source);
}

其他使用这些配置值(因此知道类型)的代码可以通过以下方式访问它们:

int foo = vec[Enum2]->get() * 7;

这似乎在大多数情况下都有效。它编译。我遇到了一些我无法解释的奇怪崩溃,这也往往会使调试器崩溃。但我对此非常怀疑,因为我不知道C++是否能够确定指向对象的真实类型(包括参数化类型),因为基类本身没有参数化。

不幸的是,在我看来,如果我对基类进行参数化,那么我基本上会消除这些 Value 类之间允许它们存储在单个容器中的共性。

我查看了 boost::any 看看是否有帮助,但我不确定它是否适用于这种情况。

在更高的层次上,我想做的是连接来自外部源(通过 API)的大量配置项,这些配置项根据项目提供不同类型的值,并将它们存储在本地,以便我的其余代码可以轻松访问它们,就好像它们是简单的数据成员一样。我也想避免写一个巨大的 switch 语句(因为这样会起作用)。

Type Erasure 可以帮我解决这个问题吗?

【问题讨论】:

  • 我什至不明白为什么int foo = vec[Enum2]-&gt;get() * 7; 会编译。你正在使用一个BaseClass 指针,它没有定义任何方法——因此你不能(合法地)调用你从vector 中取出的任何东西。你甚至不能在这里使用dynamic_cast,因为它也没有定义virtual 方法。
  • 你的怀疑是没有根据的。这种设计是一种非常常见的设计,它可以工作(前提是你在基础中有虚拟的 get 和 set 方法)。如果您遇到崩溃,请询问您的具体崩溃情况。
  • @n.m.查看上面提供的BaseParameter 的定义,告诉我在哪里可以看到virtual 函数声明。
  • @n.m.上面的句子:所以我考虑让它们基于一个(空的?)抽象基类建议不是。无论哪种方式,如果那里有方法,请包括它们;否则你的代码根本就坏了。
  • "如果我在基类中有一个 virtual get,我应该将返回类型设置为什么?" -- 与set的参数类型相同,即ParameterSource。你也可以使用boost::anyvoid* 几乎没用。

标签: c++ generics inheritance collections generic-collections


【解决方案1】:

如果您在编译时知道与每个枚举关联的类型,则可以使用boost::variant“轻松”完成此操作,无需类型擦除甚至继承。 (编辑:第一个解决方案使用了很多 C++11 功能。我在最后放置了一个不太自动化但符合 C++03 的解决方案。)

#include <string>
#include <vector>
#include <boost/variant.hpp>
#include <boost/variant/get.hpp>

// Here's how you define your enums, and what they represent:
enum class ParameterId {
  is_elephant = 0,
  caloric_intake,
  legs,
  name,
  // ...
  count_
};
template<ParameterId> struct ConfigTraits;

// Definition of type of each enum
template<> struct ConfigTraits<ParameterId::is_elephant> {
  using type = bool;
};
template<> struct ConfigTraits<ParameterId::caloric_intake> {
  using type = double;
};
template<> struct ConfigTraits<ParameterId::legs> {
  using type = int;
};
template<> struct ConfigTraits<ParameterId::name> {
  using type = std::string;
};
// ...

// Here's the stuff that makes it work.

class Parameters {
  private:
    // Quick and dirty uniquifier, just to show that it's possible
    template<typename...T> struct TypeList {
      using variant = boost::variant<T...>;
    };

    template<typename TL, typename T> struct TypeListHas;
    template<typename Head, typename...Rest, typename T>
    struct TypeListHas<TypeList<Head, Rest...>, T>
        : TypeListHas<TypeList<Rest...>, T> {
    };
    template<typename Head, typename...Rest>
    struct TypeListHas<TypeList<Head, Rest...>, Head> {
      static const bool value = true;
    };
    template<typename T> struct TypeListHas<TypeList<>, T> {
      static const bool value = false;
    };

    template<typename TL, typename T, bool B> struct TypeListMaybeAdd;
    template<typename TL, typename T> struct TypeListMaybeAdd<TL, T, false> {
      using type = TL;
    };
    template<typename...Ts, typename T>
    struct TypeListMaybeAdd<TypeList<Ts...>, T, true> {
      using type = TypeList<Ts..., T>;
    };
    template<typename TL, typename T> struct TypeListAdd
        : TypeListMaybeAdd<TL, T, !TypeListHas<TL, T>::value> {
    };

    template<typename TL, int I> struct CollectTypes
        : CollectTypes<typename TypeListAdd<TL,
                                            typename ConfigTraits<ParameterId(I)>::type
                                           >::type, I - 1> {
    };
    template<typename TL> struct CollectTypes<TL, 0> {
      using type = typename TypeListAdd<TL,
                                        typename ConfigTraits<ParameterId(0)>::type
                                       >::type::variant;
    };

  public:
    using value_type =
        typename CollectTypes<TypeList<>, int(ParameterId::count_) - 1>::type;

    template<ParameterId pid>
    using param_type = typename ConfigTraits<pid>::type;

    // It would be better to not initialize all the values twice, but this
    // was easier.
    Parameters() : values_(size_t(ParameterId::count_)) {
       clear(std::integral_constant<int, int(ParameterId::count_) - 1>());
    }

    // getter for when you know the id at compile time. Should have better
    // error checking.
    template<ParameterId pid>
    typename ConfigTraits<pid>::type get() {
      // The following will segfault if the value has the wrong type.
      return *boost::get<typename ConfigTraits<pid>::type>(&values_[int(pid)]);
    }

    // setter when you know the id at compile time
    template<ParameterId pid>
    void set(typename ConfigTraits<pid>::type new_val) {
      values_[int(pid)] = new_val;
    }

    // getter for an id known only at runtime; returns a boost::variant;
    value_type get(ParameterId pid) {
      return values_[int(pid)];
    }

  private:
    // Initialize parameters to default values of the correct type
    template<int I> void clear(std::integral_constant<int, I>) {
       values_[I] = param_type<ParameterId(I)>();
       clear(std::integral_constant<int, I - 1>());
    }
    void clear(std::integral_constant<int, 0>) {
      values_[0] = param_type<ParameterId(0)>();
    }

    std::vector<value_type> values_;
};

// And finally, a little test
#include <iostream>
int main() {
  Parameters parms;
  std::cout << ('(' + parms.get<ParameterId::name>() + ')')<< ' '
            << parms.get<ParameterId::is_elephant>() << ' '
            << parms.get<ParameterId::caloric_intake>() << ' '
            << parms.get<ParameterId::legs>() << std::endl;
  parms.set<ParameterId::is_elephant>(true);
  parms.set<ParameterId::caloric_intake>(27183.25);
  parms.set<ParameterId::legs>(4);
  parms.set<ParameterId::name>("jumbo");
  std::cout << ('(' + parms.get<ParameterId::name>() + ')')<< ' '
            << parms.get<ParameterId::is_elephant>() << ' '
            << parms.get<ParameterId::caloric_intake>() << ' '
            << parms.get<ParameterId::legs>() << std::endl;

  return 0;
}

为了那些还不能使用 C++11 的人的利益,这里有一个使用非类枚举的版本,它不够聪明,无法自己构建 boost::variant 类型,所以你必须提供它手动:

#include <string>
#include <vector>
#include <boost/variant.hpp>
#include <boost/variant/get.hpp>

// Here's how you define your enums, and what they represent:
struct ParameterId {
  enum Id {
    is_elephant = 0,
    caloric_intake,
    legs,
    name,
    // ...
    count_
  };
};
template<int> struct ConfigTraits;

// Definition of type of each enum
template<> struct ConfigTraits<ParameterId::is_elephant> {
  typedef bool type;
};
template<> struct ConfigTraits<ParameterId::caloric_intake> {
  typedef double type;
};
template<> struct ConfigTraits<ParameterId::legs> {
  typedef int type;
};
template<> struct ConfigTraits<ParameterId::name> {
  typedef std::string type;
};
// ...

// Here's the stuff that makes it work.

// C++03 doesn't have integral_constant, so we need to roll our own:
template<int I> struct IntegralConstant { static const int value = I; };

template<typename VARIANT>
class Parameters {
  public:
    typedef VARIANT value_type;

    // It would be better to not initialize all the values twice, but this
    // was easier.
    Parameters() : values_(size_t(ParameterId::count_)) {
       clear(IntegralConstant<int(ParameterId::count_) - 1>());
    }

    // getter for when you know the id at compile time. Should have better
    // error checking.
    template<ParameterId::Id pid>
    typename ConfigTraits<pid>::type get() {
      // The following will segfault if the value has the wrong type.
      return *boost::get<typename ConfigTraits<pid>::type>(&values_[int(pid)]);
    }

    // setter when you know the id at compile time
    template<ParameterId::Id pid>
    void set(typename ConfigTraits<pid>::type new_val) {
      values_[int(pid)] = new_val;
    }

    // getter for an id known only at runtime; returns a boost::variant;
    value_type get(ParameterId::Id pid) {
      return values_[int(pid)];
    }

  private:
    // Initialize parameters to default values of the correct type
    template<int I> void clear(IntegralConstant<I>) {
      values_[I] = typename ConfigTraits<I>::type();
      clear(IntegralConstant<I - 1>());
    }
    void clear(IntegralConstant<0>) {
      values_[0] = typename ConfigTraits<0>::type();
    }

    std::vector<value_type> values_;
};

// And finally, a little test
#include <iostream>
int main() {
  Parameters<boost::variant<bool, int, double, std::string> > parms;
  std::cout << ('(' + parms.get<ParameterId::name>() + ')')<< ' '
            << parms.get<ParameterId::is_elephant>() << ' '
            << parms.get<ParameterId::caloric_intake>() << ' '
            << parms.get<ParameterId::legs>() << std::endl;
  parms.set<ParameterId::is_elephant>(true);
  parms.set<ParameterId::caloric_intake>(27183.25);
  parms.set<ParameterId::legs>(4);
  parms.set<ParameterId::name>("jumbo");
  std::cout << ('(' + parms.get<ParameterId::name>() + ')')<< ' '
            << parms.get<ParameterId::is_elephant>() << ' '
            << parms.get<ParameterId::caloric_intake>() << ' '
            << parms.get<ParameterId::legs>() << std::endl;

  return 0;
}

【讨论】:

  • 这看起来很有希望,谢谢。大部分代码是通过递归模板元编程设置静态构造的列表吗?
  • 结构特化的顺序是否必须严格匹配枚举的顺序?
  • 哦,遗憾的是,我在其中运行的 SDK 环境不支持强类型枚举(C++11 功能)。但是,我确实想知道是否可以通过 boost::variant 实现我所需要的,只要我总是知道参数的类型,使用参数的代码总是会的。 IE。使用 boost::get() 来检索当前值。对于 setter 端,我也许可以使用 boost::static_visitor (因为我只需要处理少数几种类型)使用依赖于类型的 API 函数从外部值源中提取适当的值。
  • @meowsqueak:添加了一个不太安全、不太方便,但我认为功能相同的 C++03 版本。专业化的顺序在两个版本中都无关紧要。 boost::variant 中类型的顺序也无关紧要,但您必须列出所有可能的类型。确定使用哪个 boost::variant 的代码(在 C++11 版本中)是模板元编程,但它非常粗糙。希望对您有所帮助。
猜你喜欢
  • 1970-01-01
  • 2020-10-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-07
  • 1970-01-01
相关资源
最近更新 更多