是否可以在main中选择构造函数而无需编写复制构造函数?

时间:2014-03-18 12:52:03

标签: c++ c++11 scope copy-constructor move-constructor

真实的例子显然要长得多,但这总结了我的问题:

class Object
{
 int mInt1,mInt2;
 Object::Object();
 Object::Object(int param1);
 Object::Object(int param1, int param2);
};
Object::Object(){}
Object::Object(int param1):mInt1(param1){}
Object::Object(int param1, int param2):mInt1(param1),mInt1(param2){}

然后在主要:

if (type1){
  Object instance(param1);
}
else{
  Object instance(param1,param2);
}
// do stuff with instance

糟糕!这不起作用,实例超出了后续程序的范围。

Object instance;
if (type1){
  instance = Object(param1);
}
else{
  instance = Object(param1,param2);
}
// do stuff with instance

但是现在我遇到了麻烦,因为我没有定义复制构造函数。我真的不想写一个拷贝构造函数,因为我的实际类有几十个成员,其中许多是非基本类型,可能需要更多的工作来复制。

具体来说,我正在

main.cpp: error: use of deleted function ‘Object& Object::operator=(Object&&)’
         instance = Object(param1);
                  ^
note: ‘Object& Object::operator=(Object&&)’ is implicitly deleted because the default definition would be ill-formed:

9 个答案:

答案 0 :(得分:6)

处理不可复制对象的通用方法是将其抛入unique_ptr(或auto_ptr,具体取决于您的编译器)。

  std::unique_ptr<Object> instance;

  if (type1) {
    instance.reset(new Object(i));
  }
  else {
    instance.reset(new Object(i, j));
  }

在这里使用原始指针确实不安全,因为一旦你开始处理异常或任何有趣的代码路径,就会担心泄漏。相信我,在100%的情况下,如果你将它放在unique_ptr 中,你将会有更少的工作和代码行来处理。


最佳解决方案是重新设计Object的构造函数,因为规避不可复制性可能会使对象处于非法状态。通常,如果编译器认为有必要,您希望保留不可复制性。但是,我们在此处没有详细说明这种解决方案。

答案 1 :(得分:5)

如果您不想动态分配,则可以使用Initialize函数:

class Object
{
 int mInt1,mInt2;
 Object::Object();
 Object::Initialize();
 Object::Initialize(int param1);
 Object::Initialize(int param1, int param2);
};
Object::Object(){Initialize();} //call the empty Initialize for nice coding...:)
Object::Initialize(){  }
Object::Initialize(int param1){ mInt1(param1); }
Object::Initialize(int param1, int param2){ mInt1(param1);mInt1(param2);}

然后您可以使用initialize来选择类型。

Object instance;
if (type1){
  instance.Initialize(param1);
}
else{
  instance.Initialize(param1,param2);
}

答案 2 :(得分:3)

这是一个低级(ish)解决方案,可以满足您的需求。我将由您决定是否使用它是个好主意:

#include <type_traits>

template <class T>
class MultiInitialiser
{
  T *object;
  std::aligned_storage<T> storage;

public:
  MultiInitialiser() : object(nullptr)
  {}

  template <class... Arg>
  void initialise(Arg &&... arg)
  {
    if (object)
      throw "Double init error";
    object = new (&storage) T(std::forward<Arg>(arg)...);
  }

  operator T& ()
  { return *object; }

  operator const T& () const
  { return *object; }

  ~MultiInitialiser()
  {
    if (object)
      object->~T();
  }
};

以上的复制/移动操作留作读者的练习; - )

然后将使用这样的类:

MultiInitialiser<Object> instance;
if (type1){
  instance.initialise(param1);
}
else{
  instance.initialise(param1,param2);
}

除了转化为T之外,您还可以让课程operator*operator->返回包含的对象,类似于boost::optional

答案 3 :(得分:3)

有几个选项可以让您在保留自动存储的同时执行此操作,您应该使用哪个选项取决于类型Object的语义。

POD

如果您的类型与问题中给出的类型类似,您可以选择将其缩小为POD类型;基本上,删除所有用户提供的构造函数,并为所有内容提供相同的访问说明符:

struct Object {
    int mInt1, mInt2;
};

然后,您的初始化模式可能如下所示(使用placement new):

Object o; // Default-initialized, a no-op

if (condition)
    new (&o) Object {i};
else
    new (&o) Object {i, j};

常规类型

一般来说,由于移动语义,默认初始化然后分配,你的典型值语义类型将完美地运行:

std::vector <foo> v;

if (condition)
    v = std::vector <foo> (42);
else
    v = std::vector <foo> {bar, baz, quux};

但是,通常,您仍然会在默认构造函数中工作,因为某些类型&#39;默认构造的对象(例如,std::vector)具有明确定义的状态。如果你想为任意预定义类型避免这项工作,你可能想要使用std::optional(截至撰写本文时尚未标准化):

std::optional <big_but_flat> b;

if (condition)
    b.emplace (i);
else
    b.emplace (i, j);

没有std::optional

您可能会反对std::optional与此相关的开销太大,我会留给您和您的测量结果以确定是否属于这种情况。无论如何,我们可以获得我们的行为而不用担心这种开销 - 但如果你没有真正执行初始化,那么鼻子恶魔可能会怜悯。我们会使用union来获取我们想要的内容:

// At function scope
union store_v {
    std::vector <int> v;

    store_v () {}
    ~store_v () { v.~vector <int> (); }
} sv;

if (condition)
    new (&sv.v) std::vector <int> (42);
else
    new (&sv.v) std::vector <int> {49, 343, 2401};

这可能会有所改善。例如,我们可以将存储设为模板:

template <typename T>
union store {
    T t;

    store () {}
    ~store () { t.~T (); }
};

// At function scope
store <std::vector <int>> sv;
if (condition)
    new (&sv.t) std::vector <int> (42);
else
    new (&sv.t) std::vector <int> {49, 343, 2401};

我们可以给自己一个参考:

template <typename T>
union store {
    T t;

    store () {}
    ~store () { t.~T (); }
};

// At function scope
store <std::vector <int>> sv;
auto&& v = sv.t; // Deduce const, for what that's worth

if (condition)
    new (&v) std::vector <int> (42);
else
    new (&v) std::vector <int> {49, 343, 2401};

为了避免名称冲突并处理C ++的...有趣的声明语法,我们甚至可以定义一些宏来清理代码(将实现作为练习留给读取器):

template <typename T>
union store {
    T t;

    store () {}
    ~store () { t.~T (); }
};

// At function scope
DECL_UNINIT (std::vector <int>, v);

if (condition)
    INIT (v, (42));
else
    INIT (v, {49, 343, 2401});

答案 4 :(得分:2)

您可以使用指向对象的指针并通过new运算符实例化它:

Object * instance;
if (type1){
  instance = new Object(param1);
}
else{
  instance = new Object(param1,param2);
} 

答案 5 :(得分:2)

您正在使用名为 copy elision 的内容。

这表明编译器 MAY 优化代码并避免在这种情况下使用复制构造函数。 但它不必,也可能使用复制构造函数。

正确的方法(不受编译器的异想天开)将使用指针:

Object* instance;
if (type1){
  instance = new Object(param1);
}
else{
  instance = new Object(param1,param2);
}

答案 6 :(得分:1)

你可以写一个移动作业。根据您的数据成员的样子,您可能会忘记编写部分或全部内容,参见Move constructor with memcpy

那就是说,我假设您需要一整套构造函数/析构函数,包括复制和赋值;如果你想把它放在容器里,分配它等等,那将永远是必要的。否则班级不需要任何这些,你只需根据情况初始化它所需的部分,当你完成时,你手动取消初始化。

答案 7 :(得分:1)

在你的单参数构造函数的版本中,mInt2成员只是被忽略(没有被初始化),所以我假设你不会对那个成员进行任何计算{} {1}}是type1(虽然我不知道你如何在不存储false的情况下这样做。)

那么,为什么不改变dessign呢?让te构造函数以type1int param1int param2作为参数,并在内部选择如何构建自己:

type1

然后在class Object { int mInt1,mInt2; Object::Object() : mInt1(0), // don't forget to initialize your values! mInt2(0) {} // Object::Object(int param1); no more 1-parameter ctor. Object::Object(int param1, int param2, type type1) : mInt1(param1), mInt2(type1 ? param2 : 0) // assuming that 0 isn't a valid value for mInt2 {} };

main

我猜它看起来有点整洁。

答案 8 :(得分:0)

我想我找到了一种方法来做而不需要在堆上分配对象,使用lambda。 基本上,这个想法是将结构与用法分开。 (以下假设您修复了Object的定义以便编译)

auto work = []( Object& object ){ 
   // here do the work on the object
};

// now create the object and apply the work in the process:
if (type1){
  Object instance(param1);
  work( instance );
}
else{
  Object instance(param1,param2);
  work( instance );
}

这里没有涉及副本,但是无论构造对象是什么,仍然应用相同的代码,而不必声明外部函数(作为lambda是本地函数)。 从内存的角度来看,总会只有一个实例对象,因此无论路径如何,分配的堆栈内存总是相同的大小。

显然,如果实例必须超出整个功能的范围,这不起作用。如果是这种情况,那么你真的需要使用智能指针在堆上分配它。