Как скопировать (или обменять) объекты типа, который содержит члены, которые являются ссылками или константой? - PullRequest
10 голосов
/ 28 сентября 2011

Проблема, которую я пытаюсь решить, возникает при создании контейнеров, таких как std::vector объектов, которые содержат элементы данных reference и 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;
};

Это не будет работать как есть, потому что оператор присваивания по умолчанию для класса Foo не может быть создан из-за ссылочного члена foo_reference и константного члена number.

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

Я видел решения этой проблемы в Интернете в виде swap методов, которые кажутся чопорными с неопределенным поведением, основанным на чудесах reinterpret_cast и const_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
};

Это явно устраняет const ness 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));
}

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

Ответы [ 5 ]

6 голосов
/ 28 сентября 2011

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

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

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

4 голосов
/ 28 сентября 2011

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

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

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

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

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

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

Редактировать

Ответ на жалобу о неопределенном поведении.

Кажется, что проблемный случай

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

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

3 голосов
/ 28 сентября 2011

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

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

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

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

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

2 голосов
/ 28 сентября 2011

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

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

struct Bar {
   Bar (Foo & foo) : foo_reference(&foo) {}
private:
   Foo * foo_reference;
};
0 голосов
/ 28 сентября 2011

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

#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));
};
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...