有没有办法在 C++ 中同时为多个模板分配一个类型?

Is there a way to simultaneously assign a type to multiple templates in C++?

本题基于下面的示例代码,其灵感来自Sean Parent's talk。 下面代码的目标是提供一个类似于 boost::any 的对象包装器。我写这段代码是为了让自己了解类型擦除。因此,这段代码没有实际用途(考虑到已经有 boost::any)。

class ObjWrap {
public:
  template <typename T>
  ObjWrap(T O) : Self(new Obj<T>(std::move(O))) {}

  template <typename T>
  friend typename T * getObjPtr(ObjWrap O) {
    return static_cast<T*>(O.Self->getObjPtr_());
  }
private:
  struct Concept {
    virtual ~Concept() = 0;
    virtual void* getObjPtr_() = 0;
  };
  template <typename T>
  struct Obj : Concept {
    Obj(T O) : Data(std::move(O)) {}
    void* getObjPtr_() { return static_cast<void*>(&Data); }

    T Data;
  };

  std::unique_ptr<Concept> Self;
};

在我真正提出问题之前,让我们从以下几个方面检查代码:

  1. Concept::getObjPtr_ returns void* 因为 a) Concept 不能是模板,否则 unique_ptr<Concept> Self 将不起作用; b) void* 是我知道如何在 C++ 中以类型不可知的方式 return Obj::Data 的唯一方法。如有不妥请指正...

  2. T * getObjPtr(ObjWrap O) 是一个模板,需要与 ObjWrap 构造函数分开实例化。

  3. ObjWrap的使用主要包括:a) 在现有对象上创建一个新的ObjWrap; b) 检索给定 ObjWrap 的基础对象。例如:

    ObjWrap a(1);
    ObjWrap b(std::string("b"));
    int* p_a = getObjPtr<int>(a);
    std::string* p_b = getObjPtr<std::string>(b);
    

这有效,但很明显 getObjPtr<int>(b) 没有按预期工作。

所以,我的问题是:

有没有办法修复上面的代码,以便我们可以简单地使用 int* p_a = getObjPtr(a)std::string* p_b = getObjPtr(b) 或更好的 auto p_a = getObjPtr(a)auto p_b = getObjPtr(b)?换句话说,C++中有没有办法同时实例化两个模板(如果是这样,我们可以在ObjWrap对象的编译时实例化ObjWrap构造函数和T* getObjPtr(ObjWrap),例如,ObjWrap a(1))?

编辑 1:

使 ObjWrap 成为模板化 class 无济于事,因为它违背了类型擦除的目的。

template <typename T>
class ObjWrap {
  /* ... */
};

ObjWrap<int> a(1); // this is no good for type erasure. 

编辑 2:

我正在阅读代码并意识到可以对其进行修改以更好地反映这个想法。所以,也请看下面的代码:

class ObjWrap {
public:
  template <typename T>
  ObjWrap(T O) : Self(new Obj<T>(std::move(O))) {}

  template <typename T>
  T * getObjPtr() {
    return static_cast<T*>(Self->getObjPtr_());
  }
private:
  struct Concept {
    virtual ~Concept() = 0;
    virtual void* getObjPtr_() = 0;
  };
  template <typename T>
  struct Obj : Concept {
    Obj(T O) : Data(std::move(O)) {}
    void* getObjPtr_() { return static_cast<void*>(&Data); }

    T Data;
  };

  std::unique_ptr<Concept> Self;
};

int main() {
  ObjWrap a(1);
  ObjWrap b(std::string("b"));
  int* p_a = a.getObjPtr<int>();
  std::string* p_b = b.getObjPtr<std::string>();

  std::cout << *p_a << " " << *p_b << "\n";

  return 0;
}

此版本代码与上面版本的主要区别在于 T * getObjPtr() 是一个成员函数,由 ObjWrap 对象封装。

编辑 3:

我关于类型擦除的问题已被接受的答案回答。然而,关于多个模板的同时类型实例化的问题还有待回答。我的猜测是目前 C++ 不允许这样做,但很高兴听到对此有更多经验的人的意见。

  1. (etc. etc.) Please correct me if this is wrong...

你的前提至少在原则上是错误的,如果不是在实践中也是如此。您坚持要使 getObjPtr() 成为虚方法,并使用抽象基础 class。但是 - 你还没有确定这是必要的。记住——使用虚拟方法是昂贵的!为什么我要为虚拟机付费才能进行类型擦除?

Is there a way to fix the above code so that we can simply use int* p_a = getObjPtr(a)

牢记 Sean Parent 的演讲标题(与他在演讲中确实使用继承的事实相反),放弃继承,答案应该是“是”。 编辑:擦除类型的代码和un-erases类型的代码知道类型是什么就足够了——只要你不需要以 type-specific 方式对 type-erased 数据执行 。在 Sean Parent 的演讲中,你需要能够制作它的 non-trivial 副本,移动它,绘制它等等。使用 std::any/boost::any 你可能需要复制和移动,这可能需要虚拟 - 但这是最​​常见的用例。

甚至 std::any 也限制了你能做什么和不能做什么,正如这个问题中所讨论的:

why doesn't std::any_cast support implicit conversion?

有几件事可能会有所帮助。

首先要说的是,如果 Obj 需要公开对象的地址,那不是 Sean Parent 的 'inheritance is the root of all evil' 类型擦除容器。

诀窍是确保 Obj 的接口提供包装器永远需要的所有语义操作和查询。

为了提供这个,在概念中缓存对象的地址及其type_id通常是一个合理的想法。

考虑以下更新的示例,其中有一个 public 方法 - operator==。规则是如果两个 Obj 包含相同类型的对象并且这些对象比较相等,则它们相等。

注意地址和type_id:

1) 是实现细节,不会暴露在 Obj 的接口上

2) 无需虚拟调用即可访问,这会使不相等的情况短路。

#include <memory>
#include <utility>
#include <typeinfo>
#include <utility>
#include <cassert>
#include <iostream>

class ObjWrap 
{
public:
    template <typename T>
    ObjWrap(T O) : Self(new Model<T>(std::move(O))) {}

    // objects are equal if they contain the same type of model
    // and the models compare equal
    bool operator==(ObjWrap const& other) const
    {
        // note the short-circuit when the types are not the same
        // this means is_equal can guarantee that the address can be cast
        // without a further check
        return Self->info == other.Self->info
        && Self->is_equal(other.Self->addr);
    }

    bool operator!=(ObjWrap const& other) const
    {
        return !(*this == other);
    }

    friend std::ostream& operator<<(std::ostream& os, ObjWrap const& o)
    {
        return o.Self->emit(os);
    }

private:
    struct Concept 
    {
        // cache the address and type here in the concept.
        void* addr;
        std::type_info const& info;

        Concept(void* address, std::type_info const& info)
        : addr(address)
        , info(info)
        {}

        virtual ~Concept() = default;

        // this is the concept's interface    
        virtual bool is_equal(void const* other_address) const = 0;
        virtual std::ostream& emit(std::ostream& os) const = 0;
    };

    template <typename T>
    struct Model : Concept 
    {
        Model(T O) 
        : Concept(std::addressof(Data), typeid(T))
        , Data(std::move(O)) {}

        // no need to check the pointer before casting it.
        // Obj takes care of that
        /// @pre other_address is a valid pointer to a T    
        bool is_equal(void const* other_address) const override
        {
            return Data == *(static_cast<T const*>(other_address));
        }

        std::ostream& emit(std::ostream& os) const override
        {
            return os << Data;
        }

        T Data;
    };


std::unique_ptr<Concept> Self;
};


int main()
{
    auto x = ObjWrap(std::string("foo"));
    auto y = ObjWrap(std::string("foo"));
    auto z = ObjWrap(int(2));

    assert(x == y);
    assert(y != z);

    std::cout << x << " " << y << " " << z << std::endl;
}

http://coliru.stacked-crooked.com/a/dcece2a824a42948