【问题标题】:Access members of unkown template type of derived template class through pointer to base class C++通过指向基类 C++ 的指针访问派生模板类的未知模板类型的成员
【发布时间】:2017-11-17 15:57:49
【问题描述】:

我需要一个包含不同值类型的映射。我使用从B 派生的T 类型的派生模板类Derived 实现了这一点,并使用指向基类Base 的指针声明我的地图,如下所示:

class Base {
    public:
    ~Base(){}
};

template <class T>
class Derived: public Base {
     public:
     Derived(T value) {
          this->value = value;
     }
     T value;
 };

 std::map<std::string, std::shared_ptr<Base>> my_map;

 using KeyValuePair = std::pair<std::string, std::shared_ptr<Base>>;

 my_map.insert(KeyValuePair("foo1",std::make_shared<Derived<int>>(10)));
 my_map.insert(KeyValuePair("foo2",std::make_shared<Derived<double>>(20.0)));
 my_map.insert(KeyValuePair("foo3",std::make_shared<Derived<std::string>>("bla")));

例如,我现在想打印地图条目的值

 for (auto const& x : my_map)
{
    std::cout << *x.get() << std::endl;
}

但是,我现在遇到的问题是我无法通过将x 转换为Derived 来访问Derived 类中的成员,因为我不知道模板参数的类型。所以以下是不可能的:

std::shared_ptr<Derived<?>> derived = std::dynamic_cast<Derived<?>>(x);

我曾想过在Base 类中有一个返回成员的虚函数,但我不知道返回类型,所以不是一个选项。

我也不想为多个返回类型指定多个虚函数,因为代码的重点是避免复制代码并具有“通用映射”

但是,我没有看到其他解决方案,除了通过返回类似以下的流式 boost::any 来使用一些花哨的 boost 魔法,因为这会删除有关类型的任何信息。

 using streamable_any = boost::type_erasure::any<boost::mpl::vector<
 boost::type_erasure::copy_constructible<>,
 boost::type_erasure::destructible<>,
 boost::type_erasure::ostreamable<>>>;

实现我想要的目标有哪些可能性?我怀疑我需要考虑更改设计。

【问题讨论】:

  • 你有什么想要继承的理由吗?
  • 如果您可以使用 C++17,您应该查看 boost::variantstd::variant
  • 您希望对类型T 执行多少次操作?
  • T 有几种类型?列表是否在某处有界和可枚举?
  • @Yakk:我只会有几个基本类型T,不会超过10个。我只需要能够设置和获取T的值

标签: c++ templates inheritance


【解决方案1】:

特别是打印,你可以很容易地添加一个虚拟方法:

class Base {
public:
  virtual ~Base() {}

  virtual std::ostream& print(std::ostream&) const = 0;
  friend std::ostream& operator<<(std::ostream& os, Base const &b) {
    return b.print(os);
  }
};

template <class T>
class Derived: public Base {
  T value;
public:
  Derived(T v) : value(v) {}

  std::ostream& print(std::ostream& os) const override {
      return os << value;
  }
};

但是,如果您需要更一般地访问该值,您的选择基本上是:

  1. 将值提取为 Boost.any(通过虚函数)
  2. 枚举T 的所有合法类型,并返回Boost.variant&lt;Ts&gt;(同样通过虚函数)
  3. 枚举所有合法类型并为每个类型编写一个带有重载的访问者。然后在BaseDerived中实现一个简单的虚拟访问方法。请注意,如果 T 不匹配 visit 的任何重载,Derived&lt;T&gt;::apply(Visitor &amp;v) { v.visit(value); } 将无法编译,并且当 T 的有效类型具有隐式转换时您需要小心。
  4. 您可以通过编写事后访问者来避​​免将您的代码耦合到预定义的合法Ts 列表。这意味着手动调度,使用std::type_index 作为您的密钥查找要调用的函数,以及手动转换。虽然很丑。

或者(最差但最灵活)保留流输出并重新解析字符串流中的值。如果您必须这样做,请考虑将序列化为明确定义的格式,而不是(或以及)仅仅编写人类可读的格式。

【讨论】:

  • 析构函数也必须是虚拟的
  • @Slava 如果它仅在shared_ptrDerived&lt;T&gt; 中创建,则不会,因为shared_ptr 类型擦除清理代码。但这仍然可能是一个好主意。 ;)(事实上,要强制共享 ptr 位,您可能需要在 Base 中使用受保护的非虚拟析构函数)
  • @Useless 使用boost::variant 根本不应该有继承,而不是虚函数返回boost::variant 的实例
  • 替代方案是:只需使用boost::variant 而不是所有这些(在这种情况下你是对的),或者在事后添加boost:variant。我可能应该独立解决这两个问题,但不打算扩展顶级设计的替代方案。
  • @Useless:我喜欢访问者的方法,因为我需要习惯使用模式。我会尝试两种方式,visitor 和 boost::variant。
【解决方案2】:

首先你的例子中有UB,因为你通过基指针持有派生类的实例,析构函数必须是虚拟的。

要解决当前打印值的问题,您可以添加一个虚拟方法:

class Base {
    public:
    virtual void print( std::ostream &os ) const = 0;
    virtual ~Base(){}
};

但这只会解决这个特定的问题,但访问值仍然是个问题。一种解决方案是添加一个将所有值转换为std::string 的虚拟吸气剂。或者总是使用dynamic_cast&lt;&gt; 的级联(注意动态转换需要Base 中的至少一个虚拟方法)。但最好使用boost::variant,它提供了实现访问者模式的功能并检查此实例是否具有特定类型的值。在这种情况下,您根本不需要继承。

【讨论】:

  • 这个答案可以通过添加friend std::ostream&amp; operator&lt;&lt;( std::ostream&amp;, Base const&amp; )来改进。
  • @Yakk 是的,它可以,但我认为这不是一般可行的解决方案
【解决方案3】:

获取:

template<class...Ts>
struct types {};

template<class types, template<class...>class>
struct apply_to {};
template<class types, template<class...>class Z>
using apply_to_t=typename apply_to<types, Z>::type;
template<class...Ts, template<class...>class Z>
struct apply_to<types<Ts...>, Z>{ using type=Z<Ts...>; };

template<class types>
using variant_of=apply_to_t<types, boost::variant>;

using supported_types = types<int, double, std::string>;

class Base {
  virtual variant_of_t<supported_types> get() const = 0;
protected:
  ~Base() {}
};

template<class T>
class Derived final: public Base {
  T t;
public:
  virtual variant_of_t<supported_types> get() const override {
    return t;
  }
  virtual ~Derived() {}
};

设置可能有点棘手。

template<class...Ts>
using sink = boost::variant< std::function<void(Ts)>... >;

template<class types>
using setters = apply_to_t<types, sink>;

class Base {
  virtual variant_of_t<supported_types> get() const = 0;
  virtual void set( std::function<void(setters<supported_types>)> ) = 0;
protected:
  ~Base() {}
};

template<class T>
class Derived final: public Base {
  T t;
public:
  virtual void set( std::function<void(setters<supported_types>)> s ) override {
    s( std::function<void(T)>( [&](T&& in) { t = std::forward<T>(in); } ) );
  }
  virtual variant_of_t<supported_types> get() const override {
    return t;
  }
  virtual ~Derived() {}
};

这有点疯狂。

这为您提供类型安全的获取和设置。当你调用set 时,你必须传入一个可以接受任何类型设置器的 lambda,并且做正确的事情。当您调用get 时,它会返回一个变量,包含您必须处理的包含数据的可能性。

打印如下:

for (auto const& x : my_map)
{
   if (!x) continue;
   boost::apply_visitor([&](auto&& x){ 
     std::cout << x << std::endl;
   }, x->get());
}

假设你想设置一个值。

template<class...Fs>
struct override_t;
template<class F0, class F1, class...Fs>
struct override_t<F0, F1, Fs...>: F0, override_t<F1, Fs...> {
  using RBase=override_t<F1, Fs...>;
  using F0::operator();
  using RBase::operator();
  override_t( F0 f0, F1 f1, Fs... fs ):
    F0(std::move(f0)),
    RBase(std::move(f1), std::move(fs)...)
  {}
};
template<class F0>
struct override_t<F0>: F0 {
  using F0::operator();
  override_t( F0 f0 ):
    F0(std::move(f0))
  {}
};
template<>
struct override_t<> {};
template<class...Fs>
override_t<Fs...> override( Fs... fs ) { return {std::move(fs)...}; }

x->set(
  [&]( auto setters ) {
    boost::apply_visitor(
      override(
        [&]( std::function<void(int)> set ) {
          set(7);
        },
        [&]( auto&& ) {
           assert(false); // wrong type
        }
      ),
      setters
    );
  }
);

因为对未知类型的值进行类型安全设置是相当困难的。

【讨论】:

    【解决方案4】:

    一种选择是让 B 类成为模板,这样它就可以有一个返回 T 类型值的方法:

    template <class T>
    class Base {
        public:
        virtual T GetValue();
    };
    

    另一种选择是使用虚拟打印方法:

    class Base {
        public:
        virtual void PrintValue();
    };
    

    【讨论】:

    • 您的第一个解决方案将如何在提到的循环中工作?
    • 你只需在循环中调用 GetValue() ,它就会返回一个 T 类型的值
    • 不,不会。尝试实现它。
    • 请在ideone上提供示例
    • 您似乎的意思是,您不能在同一个地图中拥有具有不同模板参数的派生对象。如果它们都具有相同的类型,则它可以工作
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-27
    • 1970-01-01
    • 2011-01-27
    • 2012-09-12
    相关资源
    最近更新 更多