Подтвердить что ты не робот

Должен ли оператор присваивания соблюдать назначенный объект?

Для типов классов можно назначить временные объекты, которые на самом деле не разрешены для встроенных типов. Кроме того, оператор присваивания, сгенерированный по умолчанию, даже дает значение l:

int() = int();    // illegal: "expression is not assignable"

struct B {};
B& b = B() = B(); // compiles OK: yields an lvalue! ... but is wrong! (see below)

Для последнего оператора результат оператора присваивания фактически используется для инициализации ссылки не const, которая будет устаревать сразу после оператора: ссылка не привязана к временному объекту напрямую (он не может поскольку временные объекты могут быть привязаны только к ссылкам const или rvalue), но к результату назначения, срок жизни которого не расширяется.

Другая проблема заключается в том, что lvalue, возвращаемое из оператора присваивания, не выглядит так, как будто его можно перемещать, хотя на самом деле это относится к временному. Если что-то использует результат присваивания, чтобы получить значение, оно будет скопировано, а не перемещено, хотя было бы вполне жизнеспособным для перемещения. На этом этапе стоит отметить, что проблема описана в терминах оператора присваивания, потому что этот оператор обычно доступен для типов значений и возвращает ссылку на lvalue. Та же проблема существует для любой функции, возвращающей ссылку на объекты, т.е. *this.

Потенциальное исправление состоит в том, чтобы перегрузить оператор присваивания (или другие функции, возвращающие ссылку на объект), чтобы рассмотреть тип объекта, например:

class G {
public:
    // other members
    G& operator=(G) &  { /*...*/ return *this; }
    G  operator=(G) && { /*...*/ return std::move(*this); }
};

Возможность перегрузить операторы присваивания, как указано выше, появилась с С++ 11 и предотвратит отмеченное выше неактивное действие тонких объектов и одновременно позволит переносить результат назначения на временный. Реализация этих двух операторов, вероятно, одинакова. Хотя реализация, вероятно, будет довольно простой (по существу, всего лишь swap() двух объектов), это все равно означает дополнительную работу, поднимающую вопрос:

Если функции, возвращающие ссылку на объект (например, оператор присваивания), наблюдают значительность объекта, которому присваивается?

Альтернативно (упомянутый Simple в комментарии) состоит в том, чтобы не перегружать оператор присваивания, а чтобы явно его явно присвоить &, чтобы ограничить его использование до lvalues:

class GG {
public:
    // other members
    GG& operator=(GG) &  { /*...*/ return *this; }
};
GG g;
g = GG();    // OK
GG() = GG(); // ERROR
4b9b3361

Ответ 1

IMHO, оригинальное предложение Dietmar Kühl (предоставление перегрузок для рефлекторов & и &&) превосходит Simple один (предоставляя его только для &). Исходная идея:

class G {
public:
    // other members
    G& operator=(G) &  { /*...*/ return *this; }
    G  operator=(G) && { /*...*/ return std::move(*this); }
};

и Simple предложили удалить вторую перегрузку. Оба решения недействительны для этой строки

G& g = G() = G();

(по желанию), но если вторая перегрузка удалена, то эти строки также не скомпилируются:

const G& g1 = G() = G();
G&& g2 = G() = G();

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

Я вижу только одну ситуацию, когда предложение Simple предпочтительнее: когда G не имеет доступного конструктора copy/move. Поскольку большинство типов, для которых доступен оператор присваивания копирования/перемещения, также имеют доступный конструктор copy/move, эта ситуация встречается довольно редко.

Обе перегрузки принимают аргумент по значению, и для этого есть веские причины, если G имеет доступный конструктор copy/move. Предположим теперь, что G не имеет одного. В этом случае операторы должны принимать аргумент const G&.

К сожалению, вторая перегрузка (которая, как она есть, возвращает значение) не должна возвращать ссылку (любого типа) на *this, потому что выражение, к которому привязано *this, является rvalue и, следовательно, вероятно быть временным, чья жизнь скоро закончится. (Напомним, что запрещение этого происходить было одной из мотивов OP).

В этом случае вы должны удалить вторую перегрузку (в соответствии с предложением Simple), иначе класс не будет компилироваться (если вторая перегрузка не является шаблоном который никогда не создавался). В качестве альтернативы мы можем сохранить вторую перегрузку и определить ее как delete d. (Но зачем беспокоиться, так как существование перегрузки только для & уже достаточно?)

Периферийная точка.

Каким должно быть определение operator = для &&? (Мы снова предполагаем, что G имеет доступный конструктор copy/move.)

Как Dietmar Kühl указал, и Yakk исследовал код обоих перегрузок должен быть очень похожим, и в этом случае лучше реализовать ту, что для &&, в терминах единицы для &. Поскольку ожидается, что производительность движения будет не хуже, чем копия (и поскольку RVO не применяется при возврате *this), мы должны вернуть std::move(*this). Таким образом, возможно однострочное определение:

G operator =(G o) && { return std::move(*this = std::move(o)); }

Это достаточно хорошо, если только G можно назначить другому G или если G имеет (неявные) конструкторы преобразования. В противном случае вам следует рассмотреть возможность предоставления G (шаблона) переадресации операции копирования/перемещения назначения с универсальной ссылкой:

template <typename T>
G operator =(T&& o) && { return std::move(*this = std::forward<T>(o)); }

Хотя это не очень много кодовых табличек, это досадно, если мы должны сделать это для многих классов. Чтобы уменьшить количество кодовой таблички, мы можем определить макрос:

#define ASSIGNMENT_FOR_RVALUE(type) \
    template <typename T> \
    type operator =(T&& b) && { return std::move(*this = std::forward<T>(b)); }

Затем внутри определения G добавляется ASSIGNMENT_FOR_RVALUE(G).

(Обратите внимание, что соответствующий тип появляется только как возвращаемый тип. В С++ 14 его можно автоматически вывести компилятором и, таким образом, G и type в последних двух фрагментах кода можно заменить на auto. Из этого следует, что макрос может стать объектноподобным макросом, а не функционально подобным макросом.)

Другим способом уменьшения количества кода плиты котла является определение базового класса CRTP, который реализует operator = для &&:

template <typename Derived>
struct assignment_for_rvalue {

    template <typename T>
    Derived operator =(T&& o) && {
        return std::move(static_cast<Derived&>(*this) = std::forward<T>(o));
    }

};

Пластина котла становится наследованием и декларацией использования, как показано ниже:

class G : public assignment_for_rvalue<G> {
public:
    // other members, possibly including assignment operator overloads for `&`
    // but taking arguments of different types and/or value category.
    G& operator=(G) & { /*...*/ return *this; }

    using assignment_for_rvalue::operator =;
};

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

Ответ 2

Первая проблема заключается в том, что на С++ 03 это не совсем нормально:

B& b = B() = B();

в том, что b привязано к истекшему сроку после завершения строки.

Единственный "безопасный" способ использования этого заключается в вызове функции:

void foo(B&);
foo( B()=B() );

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

Мы можем заменить возможно неэффективный синтаксис B()=B() на:

template<typename T>
typename std::decay<T>::type& to_lvalue( T&& t ) { return t; }

и теперь вызов выглядит более понятным:

foo( to_lvalue(B()) );

который делает это посредством чистого литья. Срок службы по-прежнему не расширен (я не могу придумать, как это сделать), но мы не создаем объекты, а затем бесцельно присваиваем их другим.

Итак, теперь мы садимся и рассмотрим эти два варианта:

G operator=(G o) && { return std::move(o); }
G&&  operator=(G o) && { *this = std::move(o); return std::move(*this); }
G  operator=(G o) && { *this = std::move(o); return std::move(*this); }

которые, как и в стороне, полные реализации, предполагая, что G& operator=(G o)& существует и написано правильно. (Зачем дублировать код, когда вам это не нужно?)

Первый и третий позволяет продлить срок действия возвращаемого значения, второе использует время жизни *this. Вторая и третья модификации *this, а первая - нет.

Я бы сказал, что первый из них - правильный ответ. Поскольку *this привязано к rvalue, вызывающий заявил, что он не будет повторно использован, и его состояние не имеет значения: изменение его бессмысленно.

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

Об использовании только B operator=(B)&& заключается в том, что он позволяет относиться к коду rvalue и lvalue относительно равномерно. В качестве недостатка это позволяет относиться к нему относительно равномерно в ситуациях, когда результат может быть неожиданным.

std::forward<T>(t) = std::forward<U>(u);

вероятно, не скомпилируется вместо того, чтобы делать что-то удивительное, как "не изменяя t", когда T&& является ссылкой на rvalue. И изменение t, когда T&& является ссылкой на rvalue, также неверно.