В следующем коде, если назначение перемещения раскомментировано, функция подкачки останавливает компиляцию программы. Я наблюдал такое поведение на всех трех основных компиляторах (GCC, Clang, MSVC).
#include <utility>
#include <memory>
struct test
{
test() = default;
test(test&& other) noexcept = default;
//test& operator=(test&& other) noexcept = default;
test(const test& other)
: ptr(std::make_unique<int>(*other.ptr))
{}
test& operator=(test other) noexcept
{
std::swap(*this, other);
return *this;
}
std::unique_ptr<int> ptr;
};
Тест Godbolt: https://godbolt.org/z/v1hGzzEaz
Глядя на реализации стандартных библиотек, они используют SFINAE или концепции для включения/отключения перегрузок std::swap
, а когда специальная функция раскомментирована, по какой-то причине некоторые черты не работают (is_move_constructible
и/или is_move_assignable
в libstdc++).
Мой вопрос: почему добавление специальной функции-члена по умолчанию не позволяет стандартной библиотеке рассматривать тип как перемещаемый?
Редактирование 1: оказывается, что проблема в том, что значения x не имеют предпочтений в разрешении перегрузки между T
и T&&
(что вызывает неоднозначную ошибку перегрузки), поэтому свойства стандартной библиотеки не могут распознать тип как перемещаемый.
Редактировать 2: важно: обратите внимание, что пример кода НЕ ЯВЛЯЕТСЯ правильной реализацией идиомы копирования и замены. Это следует сделать с помощью специальной функции подкачки или использовать все 5 специальных функций-членов. Текущая реализация создает бесконечную рекурсию назначения перемещения/копирования и вызовов замены.
Мне хочется закончить как опечатка. std::swap(*this, other);
реализован с помощью move-ctor и move-assignment, здесь вы получаете бесконечную рекурсию. Должно быть std::swap(ptr, other.ptr);
.
@Botje Нет, это идиома копирования и замены. Выбор по значению является намеренным и имеет некоторые преимущества.
Примечание: копирование и замена operator=
должно быть noexcept
.
@HolyBlackCat добавил недостающий noexcept
, в отношении проблемы компиляции ничего не изменилось.
noexcept
не является причиной проблемы, поэтому я сказал «примечание». Отсутствие noexcept
просто приводит к ненужным копиям, когда класс используется как элемент std::vector
и, возможно, некоторых других контейнеров. Ваша проблема вызвана тем, что вы отправили неправильную информацию std::swap
, см. мой первый комментарий.
Извините, не внимательно прочитал вопрос. В дополнение к тому, что я сказал: вам не нужны оба operator=(test)
и operator=(const test &)
, первый заменяет и последний, и operator(test &&)
(если у вас есть конструкторы копирования и перемещения соответственно). Если вы хотите предоставить индивидуальное задание на ход, оно должно сопровождаться operator=(const test &)
, а не operator=(test)
.
В будущем я предлагаю публиковать неработающий код непосредственно в вопросе, без необходимости комментировать/раскомментировать некоторые части. Не уверен насчет других, но я обычно сначала читаю код, не обращая внимания на большую часть текста вокруг него. :/
@HolyBlackCat Правда, мне не нужны оба оператора присваивания. Правильный способ реализации копирования и обмена здесь — иметь только 4 специальные функции-члена. Но добавление назначения перемещения не должно нарушать стандартную библиотеку. Что касается поста и ваших привычек чтения — извините, это ваша проблема. Вы должны прочитать текст вокруг него. И с практической точки зрения гораздо лучше написать «раскомментируйте эту строку, чтобы разорвать ее», чем наоборот.
Основная реализация std::swap
внутренне использует назначение перемещения и выглядит примерно так:
template <typename T>
requires std::is_swappable_v<T> // exposition-only, constraint might be different
void swap(T& a, T& b) {
T c = std::move(a);
a = std::move(b);
b = std::move(c);
}
Это означает, что назначение перемещения должно быть допустимым для вашего типа, но это не так. Если бы вы вызвали оператор присваивания перемещения, вы бы получили ошибку:
<source>:18:15: error: use of overloaded operator '=' is ambiguous [...]
[...] |
<source>:9:11: note: candidate function
9 | test& operator=(test&& other) noexcept = default;
| ^
<source>:15:11: note: candidate function
15 | test& operator=(test other);
| ^
std::swap ограничен таким образом, что только типы MoveAssignable можно менять местами, а ваш — нет.
Оба оператора =
можно вызвать со значением x, и ни один из них не является лучшим совпадением.
Это связано с тем, что только преобразование lvalue в rvalue удлиняет последовательность преобразования, а инициализация объекта из значения x считается «бесплатной» (например, это не преобразование и не хуже, чем привязка ссылки). .
Даже если бы вы могли вызвать std::swap
, вы не можете одновременно
std::swap
по умолчанию, которая использует operator=
operator=
через std::swap
Это была бы бесконечная рекурсия, поскольку определение =
и std::swap
является циклическим.
Вы можете определить специальный swap
для своего типа, чтобы больше не полагаться на std::swap
.
Оставьте только operator=(test)
, который будет выглядеть так:
friend void swap(test& a, test& b) noexcept {
using std::swap; // Use swap() functions found through ADL, fall back
swap(a.ptr, b.ptr); // onto std::swap if there are none.
} // (not necessary if you only have a std::unique_ptr member,
// you could just use std::swap(a.ptr, b.ptr)).
test& operator=(test other) noexcept {
swap(*this, other); // custom function, not std::swap
return *this;
}
Вы также можете определить отдельные operator=(const test&)
и operator=(test&&)
вручную, чтобы std::swap
использовал оператор перемещения перемещения и не было двусмысленности в разрешении перегрузки.
«Оба оператора могут быть вызваны со значением x, и ни один из них не является лучшим совпадением». Это удивительно. Я ожидал, что lvalue попадет в ссылочную перегрузку const lvalue, а оба значения rvalue (prvalue и xvalue) попадут в ссылочную перегрузку rvalue.
@Xeverous Но у вас нет перегрузки ссылки на rvalue.
@HolyBlackCat Я делаю это, когда перегрузка не закомментирована. Тогда код ломается.
Извините, я, видимо, сегодня читать не смогу. Нет const T &
перегрузки, я имею в виду. Только T
и T &&
.
@HolyBlackCat Действительно. Для меня все еще сюрприз, что xvalues не имеет предпочтений.
«Вы также можете определить отдельные [...]» на самом деле мне приходится это делать, потому что текущая реализация создает бесконечную рекурсию вызовов присваивания подкачки и перемещения.
@Xeverous лично я бы все равно реализовал специальную операцию swap
для этого типа. Пользовательские swap
обычно полезны и могут оказать весьма существенное влияние на производительность, и если вы все равно определяете их самостоятельно, то вы можете делегировать operator=
им без рекурсии. Определение отдельных, полностью ручных операторов копирования/перемещения в большинстве случаев на самом деле представляет собой всего лишь микрооптимизацию.
Проблема возникла в производственном коде, но не из-за целей оптимизации, а из-за возможности копировать класс, в котором хранятся уникальные указатели (хранимые объекты имеют метод виртуального клонирования) - поэтому требовался собственный копирующий фактор. Затем я столкнулся с удивительной ошибкой компиляции.
Вы опечатали
test& operator=(test other)
, чтобы взятьother
по значению, а не по константной ссылке в реальном коде?