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

How to copy (or swap) objects of a type that contains members that are references or const?

本文关键字:const 成员 对象 类型 引用 交换 何复制 复制 包含      更新时间:2023-10-16

我试图解决的问题出现在制作容器时,例如包含引用和const数据成员的对象的std::vector:

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;
};

这不能正常工作,因为由于引用成员foo_reference和const成员number,不能构建类Foo的默认赋值操作符。

一种解决方案是将foo_reference更改为指针并去掉const关键字。然而,这失去了引用相对于指针的优势,并且const成员实际上应该是const。它们是私有成员,所以唯一可能造成伤害的是我自己的代码,但我用自己的代码搬起石头砸自己的脚(或更高)。

我已经在网络上看到了解决这个问题的swap方法的形式,这些方法似乎充满了基于reinterpret_castconst_cast的未定义行为。碰巧这些技术在我的电脑上确实有效。今天。使用特定编译器的特定版本。明天,还是用不同的编译器?谁知道呢。我不会使用依赖于未定义行为的解决方案。

关于stackoverflow的相关答案:

  • 在所有const数据成员的类中实现复制赋值操作符有意义吗?
    第一个答案有一句有趣的话:"如果那个是不可变的,你就完蛋了。"
  • 使用const成员交换方法
    第一个答案在这里并不适用,第二个答案有点拼凑。

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

编辑
为了说清楚,我已经很清楚这个解决方案了:

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

这显式地消除了numberconst性,并消除了foo_reference的隐含const性。这不是我想要的解决办法。如果这是唯一的非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));
}

,然后使用复制-交换写赋值操作符。这解决了引用和常量的问题,但这是UB吗?(至少它不使用reinterpret_castconst_cast。)一些被省略的可变数据是包含std::vector s的对象,所以我不知道像这样的浅拷贝是否会在这里工作。

不能重置引用。只需将成员存储为指针,就像在所有其他具有可赋值类的库中所做的那样。

如果你想保护自己不受自己的伤害,把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
};

(顺便说一句,它不一定是一个基类。

如果你用move操作符实现这个,有一种方法:

Bar & Bar :: operator = (Bar && source) {
    this -> ~ Bar ();
    new (this) Bar (std :: move (source));
    return *this;
}
对于复制构造函数,你不应该使用这个技巧,因为它们经常会抛出错误,这样就不安全了。Move构造函数应该永远不会ever抛出,所以这应该是可以的。

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

这种方法将允许您保留const和引用成员,但您仍然不能复制对象。要做到这一点,必须使用非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

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

是一样的
X * x = new X ();
const int & foo = x.member;
delete x;

,很明显使用foo是无效的。

也许对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

成员指针ptrx移动后仍然存在,但foo没有。

const成员应该是const

那么你不能重新赋值对象,对吗?因为这会改变你刚才说的不应该改变的值:在赋值前foo.x是1,bar.x是2,然后你执行foo = bar,那么如果foo.x"真的应该是const",那么应该发生什么?你已经告诉它修改foo.x,它真的不应该被修改。

vector的元素就像foo一样,是容器有时会修改的对象。

青春痘可能是这里的方法。动态分配一个包含所有数据成员的对象("impl"),包括const成员和引用。在vector中的对象中存储指向该对象的指针("p")。那么swap就不重要了(交换指针),move赋值也是如此,复制赋值可以通过构造一个新的impl并删除旧的来实现。

那么,Impl上的任何操作都保留数据成员的const-ness和unreseness,但是少数与生命周期相关的操作可以直接作用于p。

但是这失去了引用相对于指针的优势

没有优势。指针和引用是不同的,但没有一个是更好的。如果传递nullptr有效,则使用引用来确保存在有效的实例和指针。在你的例子中,你可以传递一个引用并存储一个指针

struct Bar {
   Bar (Foo & foo) : foo_reference(&foo) {}
private:
   Foo * foo_reference;
};

你可以组成你的类的成员照顾这些限制,但他们自己是可分配的。

#include <functional>
template <class T>
class readonly_wrapper
{
    T value;
public:
    explicit readonly_wrapper(const T& t): value(t) {}
    const T& get() const { return value; }
    operator const T& () const { return value; }
};
struct Foo{};
struct Bar {
  Bar (Foo & foo, int num) : foo_reference(foo), number(num) {}
private:
  std::reference_wrapper<Foo> foo_reference;  //C++11, Boost has one too
  readonly_wrapper<int> number;
  // Mutable member data elided
};
#include <vector>
int main()
{
  std::vector<Bar> bar_vector;
  Foo foo;
  bar_vector.push_back(Bar(foo, 10));
};

我知道这是一个比较老的问题,但我最近需要类似的东西,这让我想知道这是否有可能实现使用当代c++,例如c++ 17。我看过乔纳森·博卡拉的这篇博文并对其进行了一些改动,例如,通过添加隐式转换操作符,这就是我得到的:

#include <optional>
#include <utility>
#include <iostream>
#include <functional>
template <typename T>
class assignable
{
public:
    assignable& operator=(const assignable& rhs)
    {
        mHolder.emplace(*rhs.mHolder);
        return *this;
    }
    assignable& operator=(assignable&&) = default;
    assignable(const assignable&) = default;
    assignable(assignable&&) = default;
    assignable(const T& val)
    : mHolder(val)
    {}
    assignable(T&& val)
    : mHolder(std::move(val))
    {}
    template<typename... Args>
    decltype(auto) operator()(Args&&... args)
    {
        return (*mHolder)(std::forward<Args>(args)...);
    }
    operator T&() {return *mHolder;}
    operator const T&() const {return *mHolder;}
private:
    std::optional<T> mHolder;
};
template <typename T>
class assignable<T&>
{
public:
    explicit assignable(T& val)
    : mHolder(val)
    {}
    operator T&() {return mHolder;}
    operator const T&() const {return mHolder;}
    template<typename... Args>
    decltype(auto) operator()(Args&&... args)
    {
        return mHolder(std::forward<Args>(args)...);
    }
private:
    std::reference_wrapper<T> mHolder;
};
有些人可能会发现有争议的部分是引用类型的赋值操作符的行为,但是我认为关于重新绑定引用与底层对象的讨论已经进行了一段时间了,所以我只是根据我的口味做了。

现场演示:https://godbolt.org/z/WjW7s34jn