如何复制(或交换)包含引用或const成员的类型的对象?

Dav*_*men 10 c++ swap copy-constructor

我试图解决的问题产生于容器,例如std::vector包含引用和const数据成员的对象:

struct Foo;

struct Bar {
  Bar (Foo & foo, int num) : foo_reference(foo), number(num) {}
private:
  Foo & foo_reference;
  const int number;
  // Mutable member data elided
};

struct Baz {
  std::vector<Bar> bar_vector;
};
Run Code Online (Sandbox Code Playgroud)

这不会按原样工作,因为Foo由于引用成员foo_reference和const成员,无法构建类的默认赋值运算符number.

一种解决方案是将其更改foo_reference为指针并删除const关键字.然而,这会失去引用优于指针的优点,而该const成员确实应该这样做const.他们是私人成员,所以唯一可以造成伤害的是我自己的代码,但是我用自己的代码射击了自己的脚(或更高).

我已经看到了解决这一问题的形式在网络上swap出现的基础上的奇迹被座充满不确定的行为方式reinterpret_castconst_cast.碰巧这些技术确实在我的计算机上运行.今天.使用一个特定编译器的特定版本.明天还是用不同的编译器?谁知道.我不打算使用依赖于未定义行为的解决方案.

stackoverflow的相关问题:

那么有没有办法swap为这样一个不调用未定义行为的类编写方法/复制构造函数,或者我只是搞砸了?

编辑
只是为了说清楚,我已经非常了解这个解决方案:

struct Bar {
  Bar (Foo & foo, int num) : foo_ptr(&foo), number(num) {}
private:
  Foo * foo_ptr;
  int number;
  // Mutable member data elided
};
Run Code Online (Sandbox Code Playgroud)

这明确地消除了这一点const,number并消除了隐含const的内容foo_reference.这不是我追求的解决方案.如果这是唯一的非UB解决方案,那就这样吧.我也很清楚这个解决方案:

void swap (Bar & first, Bar & second) {
    char temp[sizeof(Bar)];
    std::memcpy (temp, &first, sizeof(Bar));
    std::memcpy (&first, &second, sizeof(Bar));
    std::memcpy (&second, temp, sizeof(Bar));
}
Run Code Online (Sandbox Code Playgroud)

然后使用copy-and-swap编写赋值运算符.这解决了引用和const问题,但它是UB吗?(至少它不使用reinterpret_castconst_cast.)一些被省略的可变数据是包含std::vectors的对象,所以我不知道这样的浅拷贝是否可以在这里工作.

vis*_*tor 6

您无法重置参考.只需将成员存储为指针,就像在具有可分配类的所有其他库中一样.

如果您想保护自己,请将int和指针移动到基类的私有部分.添加受保护的函数以仅公开int成员以进行读取和对指针成员的引用(例如,以防止自己将成员视为数组).

class BarBase
{
    Foo* foo;
    int number;
protected:
    BarBase(Foo& f, int num): foo(&f), number(num) {}
    int get_number() const { return number; }
    Foo& get_foo() { return *foo; }
    const Foo& get_foo() const { return *foo; }
};

struct Bar : private BarBase {
  Bar (Foo & foo, int num) : BarBase(foo, num) {}

  // Mutable member data elided
};
Run Code Online (Sandbox Code Playgroud)

(顺便说一句,它不一定是基类.也可以是公共访问者的成员.)


spr*_*aff 5

如果您使用移动运算符来实现此操作,则有一种方法:

Bar & Bar :: operator = (Bar && source) {
    this -> ~ Bar ();
    new (this) Bar (std :: move (source));
    return *this;
}
Run Code Online (Sandbox Code Playgroud)

你不应该在复制构造函数中使用这个技巧,因为它们经常会抛出异常,这样就不安全。移动构造函数永远不应该抛出异常,所以这应该没问题。

std::vector其他容器现在尽可能利用移动操作,因此调整大小和排序等都可以。

这种方法可以让您保留 const 和引用成员,但仍然无法复制该对象。为此,您必须使用非常量成员和指针成员。

顺便说一句,您永远不应该像非 POD 类型那样使用 memcpy。

编辑

对未定义行为投诉的回应。

问题案例似乎是

struct X {
    const int & member;
    X & operator = (X &&) { ... as above ... }
    ...
};

X x;
const int & foo = x.member;
X = std :: move (some_other_X);
// foo is no longer valid
Run Code Online (Sandbox Code Playgroud)

确实,如果您继续使用,这是未定义的行为foo。对我来说这和

X * x = new X ();
const int & foo = x.member;
delete x;
Run Code Online (Sandbox Code Playgroud)

其中很明显 usingfoo是无效的。

也许天真的阅读X::operator=(X&&)会导致你认为foo移动后也许仍然有效,有点像这样

const int & (X::*ptr) = &X::member;
X x;
// x.*ptr is x.member
X = std :: move (some_other_X);
// x.*ptr is STILL x.member
Run Code Online (Sandbox Code Playgroud)

成员指针在ptr的移动后仍然存在,xfoo没有。

  • 这仍然容易导致 UB,至少在 C++03 中是这样。关于对象生命周期的部分中有一些内容说,如果销毁并重新创建 const 对象,则之前对其进行的引用(和指针)将不再有效。这样编译器就可以缓存 const 对象的值。因此,这样做会使调用者在调用“operator=”之前持有的对“this”的任何引用无效。 (4认同)
  • @Steve,仍在 C++11 中。3.8.7 说只要你不使用 const 或引用类型就可以。我猜测提交者想要的是在某些方面保证 UB。太糟糕了。 (3认同)
  • @David:我并不是说这*不会*导致 C++11 中的 UB,只是说它*会*导致 C++03 中的 UB。我怀疑同样的语言仍然存在,我只是没有检查过。我认为你滥用了“const”的含义 - 你说你希望这个数据成员永远不会改变,但是你又说你想要将整个对象(包括该数据成员)更改为新值。这就是赋值的作用,它修改目标,因此如果目标不可修改,则不应执行此操作。这就像说您想要一个具有不可修改符号位的“int”,但您还想为其分配新值。 (2认同)
  • @edA-qamort-ora-y:谢谢,这与 C++03 相同,在上面的简短描述中我说“const 对象”,但它对 const 和引用成员的说法是相同的。至关重要的是:“原始对象的名称...可用于操作新对象,如果...对象的类型...不包含任何类型为 const 限定的非静态数据成员”。例如`Bar a; a = std::move(某物); a.number;` 是 C++11 中的 UB。这对提问者来说很糟糕,但我认为只是因为他没有完全理解 const 的真正荣耀和/或实际涉及的价值语义。 (2认同)
  • @spraff:问题案例正如我所说,“Bar a; a = std::move(某物); a.number;`,或在调用移动赋值运算符后对 `a` 的任何其他使用,都是 UB。这不是分配的预期效果,即使目标无效。`酒吧a; 酒吧 &amp;new_a = (a = std::move(something)); new_a.number;` 可以,但这不是调用者想要输入的内容。它也不能很好地与容器配合使用,而人们期望能够做到这一点,例如 `Bar &amp;b = barvec[0]; b = some;`并继续使用`b`。 (2认同)
  • @spraff:你错了。它与序列点无关,而与 const/reference 成员有关,如 edA-qa mort-ora-y 之前引用的 C++11 的 3.8/7 中所示。 (2认同)