如何允许我的模板化 Vector 允许没有默认(空)构造函数的类型?
Posted
技术标签:
【中文标题】如何允许我的模板化 Vector 允许没有默认(空)构造函数的类型?【英文标题】:How can I allow my templated Vector allow types without a default (empty) constructor? 【发布时间】:2012-01-13 04:59:21 【问题描述】:我正在创建一个模板化的 Vector 类,但是,在将其用途与 std::vector 之类的东西进行比较时,我注意到它不允许没有默认 (emtpty) 构造函数的 structs\classes。我会得到的错误是
error C2512: 'SomeStruct' : no appropriate default constructor available
: while compiling class template member function 'Vector<Type>::Vector(void)'
: see reference to class template instantiation 'Vector<Type>' being compiled
但是,如果我要使用 std::vector,这是允许的。这是我的测试用例
struct SomeStruct
SomeStruct(int a)
;
template<typename Type>
class Vector
public:
Vector();
protected:
Type* m_Data;
unsigned int m_Count;
unsigned int m_Capacity;
;
template<typename Type>
Vector<Type>::Vector()
m_Capacity = 0;
m_Count = 0;
m_Data = new Type[m_Capacity];
void main()
Vector<SomeStruct> test1;
(我知道我可以只使用 std::vector,但我这样做是为了了解有关该语言的更多信息,并遇到这样的情况)
【问题讨论】:
看看标准库是怎么做的:把分配和构造分开,自己手动构造元素。然后你可以使用任何你喜欢的构造函数。 不得不处理新的展示位置正是您不重新发明***的原因——其他人已经做得更好了。 【参考方案1】:这对于没有默认构造函数的类型不起作用的原因是因为这一行:
m_Data = new Type[m_Capacity];
上面的代码基本上做了两件事:分配足够的内存来容纳m_Capacity
的Type
实例,然后构造每个Type
以便它们可以使用。由于您实际上无法通过此new[]
语法提供任何构造函数参数,因此在使用此语法时需要默认构造函数。
std::vector
(和其他标准容器)处理这个问题的方式是将内存分配过程和构造过程分开。也就是说,std::vector
通过请求其中“无”的大块内存来分摊内存分配的成本。然后std::vector
使用placement new
直接在该内存中构造对象。
因此,std::vector
内部可能会发生类似的事情:
// HUGE SIMPLICATION OF WHAT HAPPENS!!!
// EXPOSITION ONLY!!!
// NOT TO BE USED IN ANY PRODUCTION CODE WHATSOEVER!!!
// (I haven't even considered exception safety, etc.)
template<typename T>
class Vector
private:
T* allocate_memory(std::size_t numItems)
// Allocates memory without doing any construction
return static_cast<T*>(::operator new(sizeof(T)*numItems));
void deallocate_memory()
::operator delete(buffer);
// ...
public:
void push_back(const T& obj)
if(theresNotEnoughRoom())
std::size_t newCapacity = calculateNewCapacity();
T* temp = allocate_memory(newCapacity);
copyItemsToNewBuffer(temp);
deallocate_memory(buffer);
buffer = temp;
bufferEnd = temp+newCapacity;
new (bufferEnd) T(obj); // Construct a new instance of T at end of buffer.
++bufferEnd;
void pop_back()
if(size() > 0)
--bufferEnd;
bufferEnd->~T();
// ...
private:
T* buffer;
T* bufferEnd;
// ...
;
所以这里发生的事情是我们假设的Vector
类分配了一个相对较大的内存块,然后当项目被推送或插入时,该类会在内存中放置新的。所以这消除了默认构造函数的要求,因为除非调用者请求,否则我们实际上不会构造任何对象。
正如您已经看到的那样,std::vector
类需要做大量的簿记工作才能使其工作高效且安全。这就是为什么我们敦促人们使用标准容器而不是推出自己的容器,除非您真的知道自己在做什么。制作一个高效、安全和有用的向量类是一项巨大的任务。
要了解所涉及的内容,请查看 Bjarne Stroustrup 的一篇名为 "Exception Safety: Concepts and Techniques" 的论文,该论文讨论了“简单向量”实现(第 3.1 节)。你会发现实现起来并不是一件小事。
【讨论】:
我一直在研究这个例子(这对我有很大帮助),我想我理解了大部分内容,但是,我不理解“bufferEnd”。您能否更详细地了解它是如何在新实例的构造中使用的(我已经研究了放置新,尽管在这种情况下我仍然感到困惑)。我也不明白为什么你似乎会用 ++ 和--来增加和减少它。 @mmurphy:由于我们已经解耦了内存分配和对象构造过程,因此您需要以某种方式跟踪内存的哪些部分已经有对象。由于数组是连续的,buffer
和bufferEnd-1
之间的内存包含已初始化的对象,而bufferEnd
及其之后的内存不包含任何对象。请注意,代码示例不完整,仅用于说明。如果您想更详细地讨论向量是如何实现的,请参阅this paper。我建议您阅读整篇论文。
我仍在整理所有这些内容(阅读此处的帖子、您链接的论文以及其他来源)。根据我的收集,您不是“allocate_memory”实际上将 T 初始化为任何“sizeof(T)*numItems”,同时只创建一个 T 实例吗?看起来是这样,因为您将该参数用作“可选初始化器表达式列表”,如此处所述en.wikipedia.org/wiki/Placement_new
@mmurphy:不。语法::operator new()
不调用任何构造函数。它所做的只是分配内存,所以它就像malloc()
。注意::operator new()
没有new-type-id
,所以它不可能知道调用哪个构造函数。而且由于它没有new-type-id
,我不能只告诉它“给我足够的内存来容纳T
的10 个实例”,因为它不知道T
是什么。这就是为什么那里有一个sizeof(T)*numItems
表达式。
@mmurphy:如果你在谈论 Bjarne Stroustrup 的论文,那么 ::operator delete
调用将在 vector_base::~vector_base()
的 alloc.deallocate()
函数中调用。它利用RAII idiom 确保我们在需要时不会忘记delete
内存。【参考方案2】:
new Type[m_Capacity]
创建一个m_Capacity
类型为Type
的对象的数组。那不是你想要的。您需要一个空向量,并为 m_Capacity
对象提供足够的原始内存。你不想要对象,你只想要记忆。
在 C++ 中有几种获取原始内存的工具:分配器、::operator new
或 malloc
。我建议现在使用::operator new
。
void* storage = ::operator new(sizeof(Type) * m_Capacity);
// and deallocation
::operator delete(storage);
然后,一旦您有可用的原始内存,您将需要一种在其中构造对象的方法,以实现其余的向量功能。这是使用placement-new 完成的,它是new
的一种形式,它只是在某个地址调用构造函数:
Type* obj = ::new(address) Type(arguments);
然后通过显式调用析构函数来销毁对象,因为您不想在每次销毁元素时释放内存。
obj->~T();
【讨论】:
【参考方案3】:std::vector 不使用默认构造函数,因为每次它需要构造某些东西时,它都会使用复制构造函数(或您指定的任何构造函数,感谢 Kerrek SB,下面的讨论)。因此,您可以通过在以下行中不使用默认构造函数来使您的向量类工作:
m_Data = new Type[m_Capacity];
您可以使用placement new,它可以让您在已分配的内存中构造一个对象。这使您可以调用所需的任何构造函数,例如复制构造函数。这样做是这样的:
int typeSize = sizeof(Type);
char* buffer = new char[typeSize * 2];
Type* typeA = new(buffer) Type(default_value);
Type* typeB = new(&buffer[typeSize]) Type(default_value);
这里有两件事值得注意:我们调用 new 一次,分配一块大小等于 2 个“类型”的内存。然后我们使用placement new 就地构造两个实例,而不调用默认构造函数:而是调用复制构造函数。这样,我们就可以在一个数组中构造多个实例,而无需调用默认构造函数。
最后,您需要删除原始分配,而不是使用新位置创建的分配。因为取消分配原始分配不会调用您在内存块中创建的实例的析构函数,所以您需要显式调用它们的析构函数。
【讨论】:
现代vector
s 可以使用任何你喜欢的构造函数。
@KerrekSB 啊,我的错。我在我的帖子中添加了一些修改。如果你能提供一个链接来解释这是怎么可能的,我将不胜感激。
使用emplace
或emplace_back
:v.emplace_back(arg1, arg2, arg3);
直接调用匹配的三参数构造函数。【参考方案4】:
把它放到你的代码中:
SomeStruct() = default;
创建默认构造函数。
或者这个:
SomeStruct()
同样的事情。
【讨论】:
如果他不希望他的结构有一个默认构造函数怎么办?对于某些课程,拥有一个课程是没有意义的。 根据 OP 发布的示例,他实际上想要默认构造函数,不是吗?? 没有。他想要的是让他的 Vector 类与他的结构一起工作,即使他的结构 没有 有默认构造函数。【参考方案5】:如果您从构造函数中删除m_Data = new Type[m_Capacity];
,并将此创建延迟到以后,它将起作用。但是,正如已经指出的那样,std::vector
具有相同的规则:如果您有 std::vector<SomeStruct> test1(10);
,您将得到相同的错误。
另外,void main()
很糟糕。至少应该是int main
。
【讨论】:
以上是关于如何允许我的模板化 Vector 允许没有默认(空)构造函数的类型?的主要内容,如果未能解决你的问题,请参考以下文章
为啥允许使用泛型 lambda 而不允许使用带有模板化方法的嵌套结构?
AWS IAM Cloudformation YAML 模板错误:不允许使用“空”值
mysql中,允许空,为啥int型有的可以默认为null,有的不能呢(保存时报错)?