Как копировать (или обменивать) объекты типа, содержащего элементы, которые являются ссылками или 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 не может быть создан из-за ссылочного элемента foo_reference и const member number.

Одним из решений является изменение этого foo_reference на указатель и избавление от ключевого слова const. Это, однако, теряет преимущества ссылок на указатели и что член const действительно должен быть const. Они являются частными членами, поэтому единственное, что может нанести вред, это мой собственный код, но я застрелился в ноге (или выше) с помощью своего собственного кода.

Я видел решения этой проблемы в Интернете в виде методов swap, которые кажутся переполненными поведением undefined, основанным на чудесах reinterpret_cast и const_cast. Бывает, что эти методы, похоже, работают на моем компьютере. Cегодня. С одной конкретной версией одного конкретного компилятора. Завтра или с другим компилятором? Кто знает. Я не буду использовать решение, основанное на undefined.

Похожие ответы по stackoverflow:

Итак, есть способ написать конструктор метода /t < > t29 > для такого класса, который не вызывает поведение undefined, или я просто привинчен?

Edit
Чтобы это было ясно, я уже прекрасно понимаю это решение:

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

Это явно исключает const n number и устраняет подразумеваемую const ness 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));
}

а затем запись оператора присваивания с использованием copy-and-swap. Это касается проблем ссылки и const, но это UB? (По крайней мере, он не использует reinterpret_cast и const_cast.) Некоторые из разрешенных изменяемых данных являются объектами, которые содержат std::vector s, поэтому я не знаю, будет ли здесь работать неглубокая копия.

Ответ 1

Если вы реализуете это с помощью операторов перемещения, существует способ:

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

Вы не должны использовать этот трюк с конструкторами копирования, потому что они могут часто бросать, а затем это небезопасно. Перемещение конструкторов никогда не должно бросаться, поэтому все должно быть в порядке.

std::vector, а в других контейнерах теперь, где это возможно, используются операции перемещения, поэтому изменение размера и сортировка и т.д. будут в порядке.

Этот подход позволит вам содержать константы и ссылки, но вы все равно не можете копировать объект. Для этого вам придется использовать неконстантные и указательные элементы.

И, кстати, вы никогда не должны использовать memcpy, как это для не-POD-типов.

Изменить

Ответ на жалобу Undefined Behavior.

Кажется, что проблема связана с

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

Правда, это поведение Undefined, если вы продолжаете использовать 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

Указатель-член ptr выживает при перемещении x, но foo не работает.

Ответ 2

Вы не можете переустановить ссылку. Просто сохраните элемент как указатель, как это делается во всех других библиотеках с назначаемыми классами.

Если вы хотите защитить себя от себя, переместите 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
};

(BTW, он не должен быть базовым классом. Также может быть членом с общедоступными аксессуарами.)

Ответ 3

что константный член действительно должен быть const

Ну, тогда вы не можете переназначить объект, не так ли? Поскольку это изменило бы значение того, что вы только что сказали, действительно не должно меняться: до того, как присваивание foo.x равно 1 и bar.x равно 2, и вы делаете foo = bar, тогда если foo.x "действительно должно быть const" то, что должно было случиться? Вы сказали ему изменить foo.x, который действительно не должен быть изменен.

Элемент вектора точно так же, как foo, это объект, который иногда изменяет контейнер.

Pimpl может быть здесь. Динамически выделяйте объект ( "impl" ), содержащий все ваши данные, включая константы и ссылки. Храните указатель на этот объект ( "p" ) в объекте, который находится в векторе. Тогда swap является тривиальным (свопинг указателей), как и назначение перемещения, а назначение копии может быть реализовано путем создания нового имплантата и удаления старого.

Затем любые операции с Impl сохраняют константу и непроизносимость ваших членов данных, но небольшое количество связанных с жизненным циклом операций может действовать непосредственно на P.

Ответ 4

Это, однако, теряет преимущества ссылок по указателям

Нет никакого преимущества. Указатели и ссылки разные, но ни один не лучший. Вы используете ссылку, чтобы убедиться, что есть действительный экземпляр и указатель, если передача nullptr действительна. В вашем примере вы можете передать ссылку и сохранить указатель

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

Ответ 5

Вы можете составить свой класс членов, которые позаботятся об этих ограничениях, но могут быть назначены сами.

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