依赖项没有复制 ctor 或赋值运算符时的 C++ 初始化程序列表

Posted

技术标签:

【中文标题】依赖项没有复制 ctor 或赋值运算符时的 C++ 初始化程序列表【英文标题】:C++ Initializer list when dependencies do not have copy ctor or assignment operator 【发布时间】:2021-09-02 21:25:03 【问题描述】:

方法 1

我有一些 Foo 类型,它内部包含一个 std::mutex。

class Foo 
  std::mutex m_;
;

我写了另一个类吧。 Bar 有 Foo 作为成员和构造函数,如下所示(注意 - 此代码无法编译):

class Bar 
  Bar(Foo foo) : foo(foo) 
...
private:
  Foo foo;
;

我的 IDE 抱怨:

调用 Foo 的隐式删除的复制构造函数。 'Foo' 的复制构造函数被隐式删除,因为字段 'm_' 具有已删除的复制构造函数

方法 2

然后我尝试做这样的作业:

class Bar 
  Bar(Foo fooIn)  foo = fooIn; 
...
private:
  Foo foo;
;

但这也说明了:

'Foo' 类型的对象不能被赋值,因为它的复制赋值运算符被隐式删除了

方法3(使用指针)

我可以让它工作的唯一方法是这样的:

class Foo 
private:
    std::mutex m_;
;

class Bar 
    Bar(std::unique_ptr<Foo> fooIn) : foo(std::move(fooIn)) 
private:
    std::unique_ptr<Foo> foo;
;

我了解智能指针有助于内存管理。但是让我们把这个功能放在一边。

在我上面的例子中,不使用智能指针和使用智能指针的主要区别在于智能指针具有间接性——它指向堆上的一个对象。对吧?

理解这一点的正确心智模型是什么?是不是因为 Foo 有一个已删除的复制构造函数和已删除的复制赋值,所以我唯一的选择是在堆上构造它,然后通过间接(通过指针)工作?

正确吗?还是我完全不在了?

【问题讨论】:

【参考方案1】:

是不是因为 Foo 有一个已删除的复制构造函数和已删除的复制赋值,所以我唯一的选择是让它在堆上构造,然后通过间接(通过指针)工作?

关闭,但不完全。

因为互斥体不可复制,所以默认情况下 Foo 也不可复制。您正在尝试复制,但这是不可能的。

使用间接引用存储在别处的 Foo 是可能的。但是 Foo 没有必要拥有动态存储来实现这一点。不可复制性也不意味着需要间接……除非您想防止不可复制性传播到封闭类。

互斥锁唯一的构造函数是默认构造函数。因此,您可以合理地做的只是默认初始化 Foo。一个最小的例子:

struct Bar 
    Foo foo;
    // no need to declare the default constructor
    // it is generated implicitly
;

Bar bar; // this just works

【讨论】:

啊哈!当然,这是有道理的。我可以问一个后续问题吗?如果我想为 Bar 编写一个单元测试,并且对于这个单元测试,我想为 Bar 提供一个 foo“模拟”实例,而不是让它隐式生成? @Beebunny Mocking 不适用于对象成员。你需要某种形式的多态性。通过使Bar 成为模板和Foo 模板类型参数的静态多态性,或使用间接和继承或类型擦除的动态多态性。【参考方案2】:

如果你检查互斥锁是如何实现的,你会看到它的复制构造函数和复制赋值运算符被删除了,但移动构造函数和移动赋值运算符没有被删除,这就是你的智能指针示例有效的原因。

我唯一的选择是在堆上构造它,然后通过间接(通过指针)工作?

不,这不是唯一的方法。如果您稍微更改您的第一个示例以使用引用,那也可以正常工作(并且foo 不是在堆上构造的):

class Foo 
    std::mutex m_;
;

class Bar 
    Bar(Foo& foo) : foo(foo) 

private:
    Foo& foo;
;

【讨论】:

我想做的是出售一个库,而 Bar 是面向公众的 API。如果 Bar 是我的依赖关系图的“根”,我认为 Bar 对 Foo 进行“引用”是没有意义的。 Bar 需要“拥有” Foo——不一定使用指针,但只有一个 Foo 并且它属于 Bar。如果我给 Bar 一个对 Foo 的引用,谁会拥有 Foo 并确保它不会被释放? 嗯。与指针不同,您可以将临时变量分配给const 引用,并且此临时对象在引用变量的生命周期内不会被销毁。现在,在 Visual Studio 中,您也可以为非常量引用分配一个临时对象,我猜这个临时 foo 对象将在 foo 引用存在的同时存在。但这不是标准方式。因此,例如,如果您这样做:Bar(Foo()); 将在 MSVC 中工作,但无法使用 GCC 进行编译。

以上是关于依赖项没有复制 ctor 或赋值运算符时的 C++ 初始化程序列表的主要内容,如果未能解决你的问题,请参考以下文章

复制 ctor 和赋值运算符中是不是存在语义稍有不同的问题?

C++ 中类的默认成员函数的问题(构造函数、析构函数、运算符 =、复制构造函数)(默认 ctor、dtor、复制 ctor)

派生自std :: exception的类的赋值运算符

在 C++ 中编写复制构造函数和赋值运算符的清单

删除copy-ctor和copy-assignment - public、private还是protected?

❥关于C++之类的复制构造函数&赋值运算符