我正在寻找处理不可复制对象的最佳实践。
我有一个互斥类,显然不应该是可复制的。我添加了一个私有复制构造函数来强制执行。
这破坏了代码 - 一些地方只需要修复,但我有一个通用问题,即使用互斥锁作为数据成员或通过继承的类被插入到容器中。
这通常发生在容器初始化期间,因此互斥锁尚未初始化,因此没问题,但如果没有复制构造函数,它就无法工作。将容器更改为包含指针是不可接受的。
有什么建议吗?
我正在寻找处理不可复制对象的最佳实践。
我有一个互斥类,显然不应该是可复制的。我添加了一个私有复制构造函数来强制执行。
这破坏了代码 - 一些地方只需要修复,但我有一个通用问题,即使用互斥锁作为数据成员或通过继承的类被插入到容器中。
这通常发生在容器初始化期间,因此互斥锁尚未初始化,因此没问题,但如果没有复制构造函数,它就无法工作。将容器更改为包含指针是不可接受的。
有什么建议吗?
这里有三个解决方案:
1. 使用指针- 快速解决方法是使其成为指针容器 - 例如shared_ptr
.
如果您的对象确实不可复制,并且无法使用其他容器,那将是“好”的解决方案。
2. 其他容器 - 或者,您可以使用非复制容器(使用就地构造),但是它们不是很常见并且在很大程度上与 STL 不兼容。(我已经尝试了一段时间,但它根本没有好处)
如果您的对象真正不可复制并且无法使用指针,那将是“上帝”的解决方案。
[编辑] 在 C++13 中,std::vector 允许就地构造 (emplace_back),并可用于实现移动语义的不可复制对象。[/编辑]
3. 修复你的可复制性——如果你的类是可复制的,而互斥锁不是,你“简单地”需要修复复制构造函数和赋值运算符。
编写它们很痛苦,因为您通常必须复制和分配除互斥锁之外的所有成员,但这通常可以通过以下方式简化:
template <typename TNonCopyable>
struct NeverCopy : public T
{
NeverCopy() {}
NeverCopy(T const & rhs) {}
NeverCopy<T> & operator=(T const & rhs) { return *this; }
}
并将您的互斥锁成员更改为
NeverCopy<Mutex> m_mutex;
不幸的是,使用该模板会丢失 Mutex 的特殊构造函数。
[编辑] 警告: “修复”复制 CTor/分配通常需要您锁定复制构造的右侧,并锁定分配的两侧。不幸的是,没有办法覆盖复制 ctor/assignment并调用默认实现,所以NeverCopy
如果没有外部锁定,这个技巧可能对你不起作用。(还有一些其他的解决方法有其自身的局限性。)
如果它们是不可复制的,则容器必须存储(智能)指向这些对象的指针,或引用包装器等,尽管使用 C++0x,不可复制的对象仍然可以移动(如 boost 线程),因此它们可以按原样存储在容器中。
举个例子:引用包装器(又名 boost::ref,引擎盖下的指针)
#include <vector>
#include <tr1/functional>
struct Noncopy {
private:
Noncopy(const Noncopy&) {}
public:
Noncopy() {}
};
int main()
{
std::vector<std::tr1::reference_wrapper<Noncopy> > v;
Noncopy m;
v.push_back(std::tr1::reference_wrapper<Noncopy>(m));
}
C++0x,用 gcc 测试:
#include <vector>
struct Movable {
private:
Movable(const Movable&) = delete;
public:
Movable() {}
Movable(Movable&&) {}
};
int main()
{
std::vector<Movable> v;
Movable m;
v.emplace_back(std::move(m));
}
编辑:没关系,C++0x FCD 说,在 30.4.1/3 下,
Mutex 类型不得复制或移动。
因此,您最好使用指向它们的指针。智能或必要时以其他方式包装。
如果一个对象是不可复制的,那么通常有一个很好的理由。如果有充分的理由,那么您不应该通过将其放入尝试复制它的容器来颠覆它。
考虑到你是如何构想的,这个问题没有真正的答案。没有办法做你想做的事。实际的答案是容器包含指针,并且您已经说过由于某些未指定的原因这是不正确的。
有些人谈到了可移动的东西并使用 C++0x,其中容器通常要求它们的元素是可移动的,但不要求它们是可复制的。我认为这也是一个糟糕的解决方案,因为我怀疑互斥锁在被持有时不应移动,这使得实际上无法移动它们。
因此,唯一真正剩下的答案是指向互斥体。使用::std::tr1::shared_ptr
(in #include <tr1/memory>
) 或::boost::shared_ptr
指向互斥体。这需要更改其中包含互斥锁的类的定义,但听起来您无论如何都在这样做。
使用诸如 boost::shared_ptr 之类的智能指针或使用其他容器,例如 boost::intrusive。两者都需要通过修改您的代码。
STL 容器严重依赖于它们的内容是可复制的,因此要么使它们可复制,要么不将它们放入容器中。
最好的选择是使用指针或某种在底层使用指针的包装类。这将允许这些被合理地复制,并且实际上做一个副本应该做的事情(共享互斥锁)。
但是,既然你说没有指针,还有另一种选择。听起来互斥锁“有时是可复制的”,也许您应该编写一个复制构造函数和一个赋值运算符,如果在初始化互斥锁后曾经复制过它,则让它们抛出异常。不利的一面是直到运行时才知道你做错了。
在类中使用互斥锁并不一定意味着该类必须是不可复制的。你可以(几乎)总是这样实现它:
C::C (C const & c)
// No ctor-initializer here.
{
MutexLock guard (c.mutex);
// Do the copy-construction here.
x = c.x;
}
虽然这使得复制带有互斥锁的类成为可能,但您可能不应该这样做。如果没有每个实例的互斥锁,您的设计可能会更好。
在 Ubuntu 14.04(包括 emplace_back)上使用 c++11,我已经让它工作了。
我发现emplace_back工作得很好,但是erase(可能还有insert)没有工作,因为当向量将元素改组以填补空白时,它使用:
*previous = *current;
我发现诀窍是允许在我的资源类中进行移动分配:
Watch& operator=(Watch &&other);
这是我的 inotify_watch 类,它可以存在于 std::vector 中:
class Watch {
private:
int inotify_handle = 0;
int handle = -1;
// Erases all knowledge of our resources
void erase() {
inotify_handle = 0;
handle = -1;
}
public:
Watch(int inotify_handle, const char *path, uint32_t mask)
: inotify_handle(inotify_handle),
handle(inotify_add_watch(inotify_handle, path, mask)) {
if (handle == -1)
throw std::system_error(errno, std::system_category());
}
Watch(const Watch& other) = delete; // Can't copy it, it's a real resource
// Move is fine
Watch(Watch &&other)
: inotify_handle(other.inotify_handle), handle(other.handle) {
other.erase(); // Make the other one forget about our resources, so that
// when the destructor is called, it won't try to free them,
// as we own them now
}
// Move assignment is fine
Watch &operator=(Watch &&other) {
inotify_handle = other.inotify_handle;
handle = other.handle;
other.erase(); // Make the other one forget about our resources, so that
// when the destructor is called, it won't try to free them,
// as we own them now
return *this;
}
bool operator ==(const Watch& other) {
return (inotify_handle == other.inotify_handle) && (handle == other.handle);
}
~Watch() {
if (handle != -1) {
int result = inotify_rm_watch(inotify_handle, handle);
if (result == -1)
throw std::system_error(errno, std::system_category());
}
}
};
std::vector
不能存储不可复制的对象(由于调整大小)因此你不能存储类型的对象Foo
:
struct Foo {
std::mutex mutex;
...
};
解决此问题的一种方法是使用std::unique_ptr
:
struct Foo {
std::unique_ptr<std::mutex> pmutex;
Foo() : pmutex{std::make_unique<std::mutex>()} {}
...
};
另一种选择是使用std::deque
可以保存不可复制对象的 a (例如Foo
上面第一个版本的实例。通常使用emplace_back
方法来“就地”构造对象以添加元素 - 不会发生副本。例如,Foo
这里类型的对象不需要是可复制的:
struct FooPool {
std::deque<Foo> objects;
ObjectPool(std::initializer_list<T> argList) {
for (auto&& arg : argList)
objects.emplace_back(arg);
...
};