有没有一种方法可以同时将类型分配给C ++中的多个模板?

时间:2018-08-14 21:25:18

标签: c++ templates generic-programming type-erasure

此问题基于以下示例代码,该示例代码受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_返回void*,因为a)Concept不能作为模板,否则unique_ptr<Concept> Self无效; b)void*是我知道如何在C ++中以与类型无关的方式返回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对象的编译时实例化T* getObjPtr(ObjWrap)构造函数和ObjWrap,例如ObjWrap a(1))?

编辑1:

使ObjWrap模板化类没有帮助,因为它破坏了类型擦除的目的。

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 ++不允许这样做,但是很高兴听到有更多经验的人的话。

2 个答案:

答案 0 :(得分:2)

有些事情可能会有所帮助。

首先要说的是,如果Obj曾经需要公开对象的地址,那不是Sean Parent的“继承是万恶之源”类型擦除容器。

诀窍是确保Obj的接口提供所有语义动作和包装程序将需要的查询。

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

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

请注意,地址和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

答案 1 :(得分:0)

  
      
  1. (等)。如果这是错误的,请纠正我...
  2.   

您的前提至少在原则上是错误的,如果在实践中也没有。您坚持要使getObjPtr()为虚拟方法,并使用抽象基类。但是-您尚未确定这是必要的。记住-使用虚拟方法很昂贵!为什么我要为获得类型擦除而为虚拟机付费?

  

有没有一种方法可以修复上面的代码,以便我们可以简单地使用int* p_a = getObjPtr(a)

牢记Sean Parent的谈话标题(而不是在谈话中确实使用继承的事实),放弃继承,答案应为“是”。 编辑:只要您不需要 act 以特定于类型的方式对类型擦除的数据进行处理。在Sean Parent的谈话中,您需要能够对其进行非平凡的复制,移动,绘制等。使用std::any / boost::any,您可能需要复制和移动,这可能需要虚拟机-但这是最普通的用例。

std::any甚至限制了您可以做和不能做的事情,如本问题所述:

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