C++中私有复制构造函数的作用是什么?

49

人们为什么要定义私有复制构造函数?

在什么情况下将复制构造函数和赋值运算符设为私有是一种良好的设计?

如果类中没有指向唯一对象(如文件名)的指针或句柄成员,那么除此之外,什么情况下使用私有复制构造函数是个好主意?

对于赋值运算符也适用同样的问题。鉴于大多数C ++围绕着对象的复制和引用传递,是否存在涉及私有复制构造函数的良好设计?


没有所谓的赋值构造函数,您是指移动构造函数吗? - Praetorian
这是一个打错字了...现在已经更正了...赋值运算符 - user796530
我提出这个问题的主要原因是我一直在阅读C++书籍并自学C++......而我正在阅读的书籍Lippman,Lajoie的C++ Primer和Stroustrup等都没有足够的真实世界示例来说明何时需要这种方法。当然,不可能提供详尽的清单......但可以举出一些有用的情况(例如@tc提出的汽车示例),并且我已经声明没有指针或与唯一对象(如文件)相关联。 - user796530
如果你还没有发现,Scott Meyers的书《Effective C++》非常棒 - 对于想要提高C ++技能的任何人来说都是必读之物(这是一种提供足够绳索让你自己上吊...以及周围所有人的语言)。应该在第4项中进行涵盖(从快速谷歌搜索中得知,我手头没有我的副本)。 - Luke Usherwood
你可以使用[boost :: noncopyable](http://www.boost.org/doc/libs/master/libs/core/doc/html/core/noncopyable.html)来保存大量的样板文件。像这样:`class X:private boost :: noncopyable {...}` - Luke Usherwood
7个回答

43

一个使用场景是单例模式,其中一个类只能有一个实例。在这种情况下,您需要将构造函数和赋值运算符=私有化,以确保没有创建多个对象的可能性。创建对象的唯一方法是通过下面显示的GetInstance()函数。

// An example of singleton pattern
class CMySingleton
{
public:
  static CMySingleton& GetInstance()
  {
    static CMySingleton singleton;
    return singleton;
  }

// Other non-static member functions
private:
  CMySingleton() {}                                  // Private constructor
  ~CMySingleton() {}
  CMySingleton(const CMySingleton&);                 // Prevent copy-construction
  CMySingleton& operator=(const CMySingleton&);      // Prevent assignment
};

int main(int argc, char* argv[])
{
  // create a single instance of the class
  CMySingleton &object = CMySingleton::GetInstance();

  // compile fail due to private constructor
  CMySingleton object1;
  // compile fail due to private copy constructor
  CMySingleton object2(object);
  // compile fail due to private assignment operator
  object1 = object;

  // ..
  return 0;
}

2
喔,这是一个情况……即使在这种情况下构造函数也会是私有的。 - user796530
使用C++11,我们可以将私有复制构造函数和复制赋值运算符替换为=delete版本(公共),以获得相同的结果吗? - FCo

35

有些对象代表特定实体,这些实体不能或不应该被复制。例如,您可以防止复制表示应用程序使用的日志文件的对象,相应地期望所有代码部分都使用单个日志文件。意外或不当复制对象的使用可能导致日志中出现无序内容,记录当前日志大小不准确,多次尝试(有些失败)“滚动”到新的日志文件名或重命名现有文件。

另一个用途是通过虚函数强制复制。由于构造函数不能是virtual的,常见做法是防止直接访问复制构造函数,并提供一个virtual Base* clone()方法,返回指向其实际运行时类型副本的指针。这可以防止Base b(derived)表现出的意外切片问题。

另一个例子:一个死简单的智能指针对象,只需在构造函数中给定指针即可删除它:如果它不支持引用计数或其他处理多个所有者的方式,并且不想冒险出现尴尬的意外std::auto_ptr样式的所有权转移,那么简单地隐藏复制构造函数就可以得到一个很棒的小型智能指针,对于它可用的有限情况而言,它是快速和高效的。尝试复制它时的编译时错误会有效地询问程序员“嘿-如果您真的想这样做,请将我更改为共享指针,否则请退后!”。


当将复制构造函数声明为私有时,您是否仍需要遵循三法则?也就是说,您是否仍需要一个赋值运算符和析构函数? - ThunderWiring
@ThunderWiring:简而言之,删除赋值运算符通常是一个好主意,但析构函数仍然需要被任何允许创建的实例所使用。对于赋值操作,有一些边缘情况不会有影响——比如单例模式,其中自我赋值(这是唯一可能的赋值)是安全的——但这很少有用,因此剪掉赋值运算符以避免混淆可能仍然是值得的。 - Tony Delroy

4
一个非常糟糕的例子:
class Vehicle : { int wheels; Vehicle(int w) : wheels(w) {} }

class Car : public Vehicle { Engine * engine; public Car(Engine * e) : Vehicle(4), engine(e) }

...

Car c(new Engine());

Car c2(c); // Now both cars share the same engine!

Vehicle v;
v = c; // This doesn't even make any sense; all you have is a Vehicle with 4 wheels but no engine.

“复制”一辆车是什么意思?(车是车型还是车辆实例?复制它是否会保留车辆登记信息?)

将一辆车分配给另一辆车是什么意思?

如果这些操作无意义(或仅未实现),则通常应将复制构造函数和赋值运算符设为私有,以避免出现奇怪的行为而导致编译错误。


有关车辆问题...如果我们试图将汽车公开为车辆,则该车辆对象中不会有与该汽车对应的引擎...因此在某种程度上...您应该为车辆构建一个专门的构造函数,该构造函数仅定义从汽车中取出轮子并从中构建对象...我认为,问题在于类设计而不是复制构造函数。 - user796530
2
@Jayesh Badwaik:你曾经要求真实世界的例子。稍微有些丑陋的设计在那里非常普遍,就像现实倾向于如此一样。甚至不要开始查看受特定法律和数十年官僚主义启发的代码。在那里,保持丑陋局部性更加有用。禁止复制可以帮助解决这个问题:“这个类有点奇怪,但它与表格F11-M3附录A相匹配”。 - MSalters

4

将复制构造函数和复制赋值运算符设置为私有的常见原因是禁用这些操作的默认实现。 然而,在C++ 0x中有专门的语法=delete来实现这种目的。 因此,在C++ 0x中,将复制构造函数设为私有似乎只适用于非常特殊的情况。

复制构造函数和赋值运算符只是一些语法糖; 因此,这种“私有语法糖”似乎是贪婪的表现 :)


谢谢提供这些信息,我会进一步了解的。 就个人而言,我认为应该尽可能地减少使用复制构造函数,并且绝对不应该在整个系统设计中普遍存在(影响应该尽可能小)。目前我正在处理高性能计算代码,所以我想这样做还可以。但是我仍然觉得没有它们会更好一些。 - user796530

1

"虚拟构造函数机制"是一个很重要的例子,它需要一个私有或受保护的复制构造函数。在C++中存在这样一个问题:当你得到一个指向基类的指针时,实际上它所指向的对象是从此基类继承而来的,而你想要对它进行复制。调用复制构造函数并不会调用继承类的复制构造函数,而是实际上调用了基类的复制构造函数。

请注意:

class Base {

public:
   Base( const Base & ref ){ std::cout << "Base copy constructor" ; }
};

class Derived : public Base {

public:
   Derived( const Derived & ref ) : Base(ref) { std::cout << "Derived copy constructor"; }
}

Base * obj = new Derived;
Base * obj2 = new Derived(*obj);

上面的代码将产生以下输出:

"Base copy constructor"

这显然不是程序员想要的行为!程序员试图复制一个“Derived”类型的对象,但却得到了一个“Base”类型的对象!!

使用上述习语可以解决这个问题。请注意上面写的例子,重写以使用这个习语:

class Base {

public:
  virtual Base * clone () const = 0; //this will need to be implemented by derived class

protected:
   Base( const Base & ref ){ std::cout << "Base copy constructor" ; }
};

class Derived : public Base {

public:
  virtual Base * clone () const {

    //call private copy constructor of class "Derived"
    return static_cast<Base *>( new Derived(*this) );
  }

//private copy constructor:
private:
   Derived( const Derived & ref ) : Base(ref) { std::cout << "Derived copy constructor"; }
}

Base * obj = new Derived;
Base * obj2 = obj->clone();

上面的代码将会产生如下输出:
"Base copy constructor"
"Derived copy constructor"

换句话说,构造的对象是所需类型“Derived”,而不是类型“Base”!

正如您所看到的,在Derived类型中,拷贝构造函数被故意设置为私有,因为给程序员提供手动调用拷贝构造函数的能力而不是使用clone()提供的巧妙接口会导致糟糕的API设计。换句话说,可直接调用的公共拷贝构造函数可能会导致程序员犯下第1部分提到的错误。在这种情况下,最佳实践是将拷贝构造函数隐藏起来,只能通过使用方法“clone()”间接访问。


您的第一个示例无法编译,因为C++中不存在隐式向下转换(Base * obj2 = new Derived(obj) 会生成错误,obj 无法隐式转换为Derived&)。所以不清楚您尝试使用虚构造函数惯用语解决什么问题。 - user396672

1
即使对象的内容不是指针或其他引用,防止人们复制对象仍然很有用。也许该类包含大量数据,而复制操作太重了。

是的,我明白...但这应该留给程序员来决定,对吧?而不是强制禁止复制,程序员可以通过引用访问对象... - user796530
@Jayesh:一个好的API很难被误用(或者说是不可能的)。你仍然可以通过引用访问对象,但这并不意味着你应该被允许复制它。 - Nicol Bolas

0

你可能想要使用复制构造函数来实现类的某些方法,但不想将其暴露在类外部。因此,你可以将其设置为私有方法,就像其他方法一样。


这是我的问题...确切地说是为什么呢? 可能有不同的原因... 如果您能提供一些例子会很有帮助... - user796530

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接