Почему добавление оператора присваивания перемещения по умолчанию нарушает компиляцию стандартной функции подкачки?

В следующем коде, если назначение перемещения раскомментировано, функция подкачки останавливает компиляцию программы. Я наблюдал такое поведение на всех трех основных компиляторах (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 специальных функций-членов. Текущая реализация создает бесконечную рекурсию назначения перемещения/копирования и вызовов замены.

Вы опечатали test& operator=(test other), чтобы взять other по значению, а не по константной ссылке в реальном коде?

Botje 15.09.2023 10:57

Мне хочется закончить как опечатка. std::swap(*this, other); реализован с помощью move-ctor и move-assignment, здесь вы получаете бесконечную рекурсию. Должно быть std::swap(ptr, other.ptr);.

HolyBlackCat 15.09.2023 10:57

@Botje Нет, это идиома копирования и замены. Выбор по значению является намеренным и имеет некоторые преимущества.

HolyBlackCat 15.09.2023 10:58

Примечание: копирование и замена operator= должно быть noexcept.

HolyBlackCat 15.09.2023 10:58

@HolyBlackCat добавил недостающий noexcept, в отношении проблемы компиляции ничего не изменилось.

Xeverous 15.09.2023 11:08
noexcept не является причиной проблемы, поэтому я сказал «примечание». Отсутствие noexcept просто приводит к ненужным копиям, когда класс используется как элемент std::vector и, возможно, некоторых других контейнеров. Ваша проблема вызвана тем, что вы отправили неправильную информацию std::swap, см. мой первый комментарий.
HolyBlackCat 15.09.2023 11:10

Извините, не внимательно прочитал вопрос. В дополнение к тому, что я сказал: вам не нужны оба operator=(test) и operator=(const test &), первый заменяет и последний, и operator(test &&) (если у вас есть конструкторы копирования и перемещения соответственно). Если вы хотите предоставить индивидуальное задание на ход, оно должно сопровождаться operator=(const test &), а не operator=(test).

HolyBlackCat 15.09.2023 11:20

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

HolyBlackCat 15.09.2023 11:22

@HolyBlackCat Правда, мне не нужны оба оператора присваивания. Правильный способ реализации копирования и обмена здесь — иметь только 4 специальные функции-члена. Но добавление назначения перемещения не должно нарушать стандартную библиотеку. Что касается поста и ваших привычек чтения — извините, это ваша проблема. Вы должны прочитать текст вокруг него. И с практической точки зрения гораздо лучше написать «раскомментируйте эту строку, чтобы разорвать ее», чем наоборот.

Xeverous 15.09.2023 11:27
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
9
74
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Основная реализация 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 15.09.2023 11:34

@Xeverous Но у вас нет перегрузки ссылки на rvalue.

HolyBlackCat 15.09.2023 11:37

@HolyBlackCat Я делаю это, когда перегрузка не закомментирована. Тогда код ломается.

Xeverous 15.09.2023 11:43

Извините, я, видимо, сегодня читать не смогу. Нет const T & перегрузки, я имею в виду. Только T и T &&.

HolyBlackCat 15.09.2023 11:50

@HolyBlackCat Действительно. Для меня все еще сюрприз, что xvalues ​​не имеет предпочтений.

Xeverous 15.09.2023 11:52

«Вы также можете определить отдельные [...]» на самом деле мне приходится это делать, потому что текущая реализация создает бесконечную рекурсию вызовов присваивания подкачки и перемещения.

Xeverous 15.09.2023 13:01

@Xeverous лично я бы все равно реализовал специальную операцию swap для этого типа. Пользовательские swap обычно полезны и могут оказать весьма существенное влияние на производительность, и если вы все равно определяете их самостоятельно, то вы можете делегировать operator= им без рекурсии. Определение отдельных, полностью ручных операторов копирования/перемещения в большинстве случаев на самом деле представляет собой всего лишь микрооптимизацию.

Jan Schultke 15.09.2023 13:29

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

Xeverous 15.09.2023 15:43

Другие вопросы по теме