Как скопировать (или поменять местами) объекты типа, который содержит элементы, являющиеся ссылками или константами?

Проблема, которую я пытаюсь решить, возникает при создании контейнеров, таких как 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 и константного члена 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 сущность 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));
}

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


person David Hammen    schedule 28.09.2011    source источник
comment
Не могли бы вы сделать новое размещение для этого, вызвав тем самым конструктор копирования (или специальный конструктор подкачки)?   -  person edA-qa mort-ora-y    schedule 28.09.2011
comment
@ edA-qa mort-ora-y: Хммм. Я, конечно, могу написать действительный конструктор копирования. Покажите мне!   -  person David Hammen    schedule 28.09.2011
comment
Во-первых, вы не можете запоминать ссылку. C ++ 11 говорит, что структура со ссылкой не является типом стандартного макета, поэтому memcpy для ссылки - это UB.   -  person edA-qa mort-ora-y    schedule 28.09.2011
comment
Посмотрите ответ Спраффа. Он использует версию с перемещением, но вы можете сделать то же самое, используя обычный конструктор / присваивание копии (без перемещения).   -  person edA-qa mort-ora-y    schedule 28.09.2011
comment
Я посмотрел. Я поддержал. Я еще не принял это; Какое-то время я придерживаюсь решения C ++ 03. Поскольку я не говорил никаких решений C ++ 11, пожалуйста, было бы несправедливо отвергать это решение, если оно единственное, что является действительным и позволяет избежать отхода от элементов данных reference / const.   -  person David Hammen    schedule 28.09.2011
comment
В любом случае, не должны ли классы с const- и ссылочными членами быть noncopyable? По крайней мере, это то, что я узнал. Тем не менее, move-swap должен работать нормально, но это не C ++ 03.   -  person Xeo    schedule 28.09.2011


Ответы (5)


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

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 - нет.

person spraff    schedule 28.09.2011
comment
Я думаю, что это отлично работает и для копирования (небольшое изменение синтаксиса) - я не думаю, что он имел в виду, что не хочет иметь возможность копировать. Я пытаюсь выяснить, допустимо ли новое размещение для всех типов объектов, а если нет, то какие? - person edA-qa mort-ora-y; 28.09.2011
comment
Это близко к тому, что мне нужно. Проголосовали за, так как я не сказал C ++ 03 только в своем вопросе. Есть ли способ сделать это, не переходя на C ++ 11? (У меня нет такой роскоши. Наш код должен компилироваться с помощью компилятора C ++ 03.) - person David Hammen; 28.09.2011
comment
Re И, кстати, никогда не следует использовать memcpy подобным образом для типов, отличных от POD. Я знаю это. Я разместил это в основном потому, что подобный код широко распространен в сети; Я не хотел, чтобы кто-то предлагал это как решение. Моя текущая реализация - это вариант A, не используйте ссылки и константные члены данных. Вариант B, вызывающий неопределенное поведение, для меня не запускается. Я бы хотел вариант C, волшебную версию, которая работает в C ++ 03 и делает это без вызова опции A и без вызова UB. - person David Hammen; 28.09.2011
comment
Это все еще может привести к UB, по крайней мере, в C ++ 03. В разделе о жизненном цикле объекта есть что-то, в котором говорится, что если вы уничтожите и воссоздаете константный объект, то ссылки (и указатели) на него, взятые заранее, больше не будут действительны. Это сделано для того, чтобы компилятор мог кэшировать значения константных объектов. Таким образом, это делает недействительными любые ссылки на this, которые вызывающий имел перед вызовом operator=. - person Steve Jessop; 28.09.2011
comment
Но копирование не проблема даже в C ++ 03, верно? Bar(const Bar &other) : foo_reference(other.foo_reference), number(other.number) {} отлично работает. Единственная проблема - это назначение. Так вам действительно нужен конструктор перемещения? Не могли бы вы просто использовать новое размещение с конструктором копирования? - person Nemo; 28.09.2011
comment
@Nemo: Что-то вроде Bar& Bar::operator=(const Bar & src) {this->~Bar(); new(this) (src); return *this;}? Это очень близко, но что, если конструктор копирования выдает ошибку? - person David Hammen; 28.09.2011
comment
@Steve: Из вашего комментария кажется, что в C ++ 03 нет опции C. IMO, PIMPL, внутренний класс, управляемый с помощью автоматических указателей, унаследованный от базового класса, ...: Это обходные пути, чтобы избежать дыры в языке. C ++ 11 исправляет эту дыру с помощью семантики перемещения. Итак, я пока остановлюсь на варианте А (наименьшая путаница вокруг дыры), добавлю отложенный тикет, чтобы исправить, когда мы, наконец, перейдем на C ++, ... и приму этот ответ. - person David Hammen; 28.09.2011
comment
@David: Я не утверждаю, что это не приводит к UB в C ++ 11, просто это делает в C ++ 03. Я подозреваю, что тот же язык все еще там, я просто не проверял. Я думаю, вы злоупотребляете значением const - вы говорите, что хотите, чтобы этот член данных никогда не изменялся, но затем вы говорите, что хотите изменить весь объект, включая этот член данных, на новые значения. Это то, что делает присваивание: оно изменяет цель, поэтому, если цель неизменяема, вы не должны этого делать. Это все равно, что сказать, что вам нужен int с неизменяемым битом знака, но вы также хотите присвоить ему новые значения. - person Steve Jessop; 28.09.2011
comment
@Steve, все еще на C ++ 11. 3.8.7 говорит, что это нормально, если вы не используете константные или ссылочные типы. Я предполагаю, что все, что хочет отправитель, в некотором отношении гарантировано UB. Это отстой. - person edA-qa mort-ora-y; 28.09.2011
comment
@ edA-qamort-ora-y: спасибо, это то же самое, что и C ++ 03, в моем кратком описании выше я сказал объект const, но он говорит то же самое о константных и ссылочных членах. Важно отметить: имя исходного объекта ... может использоваться для управления новым объектом, если ... тип объекта ... не содержит каких-либо нестатических элементов данных, тип которых определен как const. Так, например, Bar a; a = std::move(something); a.number; - это UB в C ++ 11. Это отстой для вопрошающего, но я думаю, только потому, что он не в полной мере осознает истинное величие константы и / или то, что на самом деле включает семантика значений. - person Steve Jessop; 28.09.2011
comment
@SteveJessop, я могу понять причину, по которой const подпадает под это правило, но эта ссылка также затронута, похоже, не имеет смысла. Комитет должен все еще думать о некоторой теоретической системе, которая не просто реализует ссылки как указатели. Глупый. - person edA-qa mort-ora-y; 28.09.2011
comment
@ edA-qamort-ora-y: Или они думают о реальных системах, реализующих ссылки T& как T *const. Проблема в том, что язык говорит, что ссылки не могут быть повторно размещены, и поэтому оптимизаторы продолжат работу на этом основании. Если бы их можно было переставить, им бы не пришлось играть ту же роль, что и const, нарушая задания. Итак, я думаю, вы в основном говорите глупо, что ссылки не могут быть повторно заменены, что IMO не глупо, но является целым отдельным аргументом от того, действителен ли этот код :-) - person Steve Jessop; 28.09.2011
comment
@ edA-qamort-ora-y: Большинство систем реализуют ссылки как указатели ... const указатели :-) - person Nemo; 28.09.2011
comment
@SteveJessop, это не должно вызвать проблем у оптимизатора. Значение указателя не изменяется, только значение, на которое он указывает. Ничто не пытается изменить адрес. - person edA-qa mort-ora-y; 28.09.2011
comment
@ edA-qamort-ora-y: Понятно, так что, если Bar::foo_reference является элементом ссылочных данных, то вы говорите, что присвоение экземпляру Bar должно назначать рефераду foo_reference, а не повторно размещать ссылку? Если это то, что вы хотите, вы можете просто сделать это в операторе присваивания, нет необходимости в этом бизнесе деструкции и построения. Ничто в языке вас не останавливает, просто это не то, что делает оператор присваивания по умолчанию. Думаю, думали, что это будет немного удивительно, например Если дочерний элемент содержит ссылку на родительский элемент, вы не ожидаете, что присвоение дочернему элементу изменит родительский элемент. - person Steve Jessop; 28.09.2011
comment
См. Правки за мои 2 цента по этой ссылке malarky. @Steve, я думаю, вы жалуетесь на случаи использования, которые не работают, независимо от того, используется ли техника из моего ответа. Это еще одна проблема с висячими ссылками, и разговор о том, что оптимизатор может сделать с неправильным кодом, упускает суть. - person spraff; 28.09.2011
comment
@spraff: проблема в том, что, как я уже сказал, Bar a; a = std::move(something); a.number; или любое другое использование a после вызова оператора присваивания перемещения - это UB. Это не ожидаемый эффект от назначения, чтобы сделать цель недействительной. Bar a; Bar &new_a = (a = std::move(something)); new_a.number; было бы хорошо, но вызывающие абоненты хотят набирать не это. Это также не очень хорошо работает с контейнерами, где люди ожидают, что смогут сделать, например, Bar &b = barvec[0]; b = something; и продолжить использовать b. - person Steve Jessop; 28.09.2011
comment
Ссылка на a после перехода в него - это нормально. Там есть точка последовательности! Если бы это было проблемой, это было бы проблемой для всех Bar, даже без элементов const / reference. - person spraff; 28.09.2011
comment
@spraff: вы ошибаетесь. Это не имеет ничего общего с точками последовательности и все, что связано с элементами const / reference, как в 3.8 / 7 C ++ 11, упомянутом ранее edA-qa mort-ora-y. - person Steve Jessop; 28.09.2011
comment
@ edA-qamort-ora-y Комитет должен все еще думать о какой-то теоретической системе Нет. Ссылка не может быть повторно привязана, так же как значение const не может быть изменено. - person curiousguy; 23.07.2015

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

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

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

person visitor    schedule 28.09.2011
comment
Это решение, которое я уже реализовал. Было бы неплохо иметь решение, которое не связано с изменением ссылки на указатель или удалением квалификатора const. - person David Hammen; 28.09.2011
comment
@David: Это решение обязательно потребует изменения значения const и повторной установки ссылки, что вы просто не можете сделать по закону. - person visitor; 28.09.2011
comment
@David: И ничто в вашем вопросе, похоже, не указывает на то, что вы знаете об этом решении. Что касается Bar, foo является (доступен только как) ссылкой, а number (доступен только как) const. - Если вы говорите, что хотите пойти еще дальше, то это становится параноиком. - person visitor; 28.09.2011
comment
Об этом с самого начала задавался вопрос: Одно из решений - изменить эту foo_reference на указатель и избавиться от ключевого слова const. - person David Hammen; 28.09.2011
comment
@David: Но вам, кажется, не хватает того, что это все в базовом классе / классе-члене. Неконстантное целое число и указатель полностью недоступны для вашего класса. BarBase - это весь класс, ограничение доступа при разрешении назначения - это все, что он делает. - person visitor; 28.09.2011
comment
Я вижу, что вы здесь делаете. Как и в случае с ответом Стива, это жизнеспособно, но для меня менее желательно, чем, по общему признанию, громоздкий вариант A. Не желательно, потому что больше кода, другой класс для тестирования, и это тоже путаница вокруг дыры в языке. Итак, проголосовали за, но не выбрали. - person David Hammen; 28.09.2011
comment
Это дыра в языке, которая не позволяет вам переставлять ссылки и изменять константы ?! Или вы имеете в виду, что это дыра в языке, в которой нет дыры здесь, тогда как обычно дыра есть для всего? - person UncleBens; 28.09.2011

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

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

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

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

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

person Steve Jessop    schedule 28.09.2011
comment
Проголосовали за жизнеспособное решение, но не выбрали. Я не большой поклонник PIMPL, и это своего рода путаница вокруг дыры в языке, которую решает C ++ 11. - person David Hammen; 28.09.2011
comment
Я не думаю, что C ++ 11 решает эту проблему, хотя могу ошибаться. - person Steve Jessop; 28.09.2011
comment
Если вы не хотите скрыть объявление и реализацию impl, pimpl функционально эквивалентен классу с отключенным копированием и назначением, управляемым с помощью unique_ptr или shared_ptr в зависимости от потребностей. - person Emilio Garavaglia; 29.09.2011
comment
@Emilio: да, разница только в том, выбирает ли пользователь интеллектуальный указатель или публичный API представляет что-то с семантикой значения, которое, как правило, включает какой-то интеллектуальный указатель под крышкой. - person Steve Jessop; 29.09.2011

Однако при этом теряются преимущества ссылок перед указателями.

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

struct Bar {
   Bar (Foo & foo) : foo_reference(&foo) {}
private:
   Foo * foo_reference;
};
person hansmaad    schedule 28.09.2011
comment
Я уже этим занимаюсь и по возможности хотел бы этого избежать. В сети есть множество реализаций swap, которые выполняют обмен ссылками, но они вызывают UB. Он работает на их компьютере с их компилятором, и на моем тоже. Но это все равно УБ. - person David Hammen; 28.09.2011
comment
Ссылки разные. Они могут давать один и тот же окончательный машинный код, но семантика различается. - person spraff; 28.09.2011
comment
Точно. Ссылка как член данных по поведению довольно близка к указателю const, который заведомо не равен нулю. Изменение ссылки на указатель const не решает проблемы; это все еще const. - person David Hammen; 28.09.2011
comment
Выражение «преимущества X над Y» обычно относится к любым различиям, которые выгодны пользователю для конкретной цели. Сказать, что X имеет преимущества перед Y, не означает, что X строго лучше Y во всех отношениях. В самом деле, такая же разница может быть преимуществом X перед Y для одной цели, но преимуществом Y перед X для другой. - person Steve Jessop; 28.09.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));
};
person UncleBens    schedule 28.09.2011