Re[3]: placement new
От: rg45 СССР  
Дата: 11.12.22 21:38
Оценка: 27 (2)
Здравствуйте, maks1180, Вы писали:

R>>Согласно стандарту языка, использование одной и той же области памяти для размещения двух разных объектов порождает неопределенное поведение. Нельзя так делать.


M>Даже если первый объект больше не будет использован после создания второго ?


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

https://timsong-cpp.github.io/cppwp/basic.life#5

A program may end the lifetime of an object of class type without invoking the destructor, by reusing or releasing the storage as described above.


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

In this case, the destructor is not implicitly invoked and any program that depends on the side effects produced by the destructor has undefined behavior.


То есть, не UB в чистом виде, но программирование на грани фола, что называется. Без особой необходимости пускаться в такие авантюры лично я бы не стал.
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 11.12.2022 21:41 rg45 . Предыдущая версия .
Re[4]: placement new
От: σ  
Дата: 12.12.22 10:20
Оценка: 9 (2)
R>Но надо понимать, что деструктор объекта при этом не вызывается, а вот это уже может порождать неопределенное поведение. В этом же пунтке сказано вот что:

R>

R>In this case, the destructor is not implicitly invoked and any program that depends on the side effects produced by the destructor has undefined behavior.


https://cplusplus.github.io/CWG/issues/2523.html
Re: placement new
От: vopl Россия  
Дата: 10.12.22 17:50
Оценка: +2
Здравствуйте, maks1180, Вы писали:

M>Допустим есть 2 класса:

M>Class A {
M>public: int v1; int v2;
M>};
M>class B: public Class A {
M> int v3;
M> B() { v3 = 1; }
M>};

куски кода желательно оформлять именно как C/C++ код, ато их читать неудобно, глаза режет ..
Re: placement new
От: rg45 СССР  
Дата: 10.12.22 17:58
Оценка: :)
Здравствуйте, maks1180, Вы писали:

M>сначала я создал экземпляр сlass A через placement new

M>new(p)A();
M>поработал с ним, потом создал сlass B, на тот же месте где был Class A
M>new(p)B();

M>1) Есть ли гарантии, что v1 и v2 класс A и В будут по одинаковым смещениям ?

M>2) Есть ли гарантии, что конструктор класса B не перетрёт значения v1, v2 которые остались в памяти от прошлого экземпляра класса A ?

Согласно стандарту языка, использование одной и той же области памяти для размещения двух разных объектов порождает неопределенное поведение. Нельзя так делать.

P.S. Был не прав, каюсь. Подробности здесь: http://rsdn.org/forum/cpp/8426197.1
Автор: rg45
Дата: 12.12.22
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 11.12.2022 21:40 rg45 . Предыдущая версия .
Re: placement new
От: Sm0ke Россия ksi
Дата: 10.12.22 22:42
Оценка: +1
Здравствуйте, maks1180, Вы писали:

Я бы предложил сделать обёртку, которая хранит экземпляр для смены. Назовём его keep

https://godbolt.org/z/34j8s6rxY
#include <concepts>
#include <type_traits>
#include <algorithm>

template <typename T, typename ... U>
concept c_any_of = ( std::same_as<T, U> || ... );

template <typename T_base, std::derived_from<T_base> ... T_sub>
struct keep {
  static constexpr std::size_t
  s_max_size = std::max<std::size_t>({sizeof(T_base), sizeof(T_sub) ...});
  
  static constexpr std::size_t
  s_max_align = std::max<std::size_t>({alignof(T_base), alignof(T_sub) ...});
  
  static constexpr bool
  s_all_trivial_des = (
    std::is_trivially_destructible_v<T_base> &&
    (std::is_trivially_destructible_v<T_sub> && ...)
  );
  
  using t_data = std::aligned_storage_t<s_max_size, s_max_align>;
  using pointer = T_base *;
  
  private:
  t_data m_data;
  public:
  pointer m_base = nullptr;
  
  ~keep() requires(! s_all_trivial_des) {
    clear();
  }

  template <c_any_of<T_base, T_sub ...> T, typename ... T_args>
  T * emplace(T_args && ... p_args) {
    clear();
    T * ret = new (&m_data) T(std::forward<T_args>(p_args) ...); 
    m_base = ret;
    return ret;
  }
  
  operator bool () const {
    return m_base != nullptr;
  }

  pointer operator -> () {
    return m_base;
  }

  void clear() {
    if( m_base != nullptr ) {
      if constexpr( ! s_all_trivial_des ) { m_base->~T_base(); }
      m_base = nullptr;
    }
  }
};


Тут keep<some_base, some_derived1, some_derived2> Хранит указатель на базовый класс в поле m_base , Ведь нет гарантий, что адреса в указателе при разных emplace<some_derived_xxx>() будут одинаковыми. Например при множественном наследовании от других классов. Кроме того при (m_base == nullptr) мы точно знаем, что объект keep пуст.

Если надо в keep создать сперва базовый класс, а потом пересоздать один из наследников, но свойства базового сохранить, то я думаю стоит эти свойства объединить в отдельную структуру. Чтобы можно было их засэйвить (скопировать или мувнуть) в локальную переменную и потом пробросить в конструктор наследника через emplace<xd>(v_params).

И напоследок: Если у целевых классов есть нетривиальный деструктор, то в базовом деструктор должен быть виртуальным.
Отредактировано 10.12.2022 23:11 Sm0ke . Предыдущая версия . Еще …
Отредактировано 10.12.2022 22:55 Sm0ke . Предыдущая версия .
Отредактировано 10.12.2022 22:53 Sm0ke . Предыдущая версия .
Отредактировано 10.12.2022 22:51 Sm0ke . Предыдущая версия .
Отредактировано 10.12.2022 22:47 Sm0ke . Предыдущая версия .
Отредактировано 10.12.2022 22:45 Sm0ke . Предыдущая версия .
Re: placement new
От: vopl Россия  
Дата: 11.12.22 08:20
Оценка: +1
Здравствуйте, maks1180, Вы писали:

M>Допустим есть 2 класса:

M>сlass A {
M>public: int v1; int v2;
M>};

M>class B: public сlass A {

M> int v3;
M> B() { v3 = 1; }
M>};

M>сначала я создал экземпляр сlass A через placement new

M>new(p)A();
M>поработал с ним, потом создал сlass B, на тот же месте где был Class A
M>new(p)B();

M>1) Есть ли гарантии, что v1 и v2 класс A и В будут по одинаковым смещениям ?

M>2) Есть ли гарантии, что конструктор класса B не перетрёт значения v1, v2 которые остались в памяти от прошлого экземпляра класса A ?

M>Таким образом я хочу динамически изменить класс объекта сохранив его в той же области памяти.

M>Или может можно как-то поменять v-table у объекта и он станет объектом другого класса ?

M>Такое изощерение для того что-бы не вызывать epoll_ctl(..., EPOLL_CTL_MOD) при смене адреса, который занимает около 4100 тактов.


Желаемых гарантий нет. Но, первую можно самому обеспечить если правильно сформировать лайаут, да еще и ассертами его провалидировать. По поводу второй — наоборот, есть гарантия что конструирование нового объекта на месте имеющегося — UB, так точно делать нельзя. Но, если сначала разрушить оригинаотный объект а потом в этой памяти построить новый — то так вполне можно делать. В общем, если аккуратно все оформить — то в принципе все получится
#include <memory>

/////////0/////////1/////////2/////////3/////////4/////////5/////////6/////////7
class A
{
public:
    int v1;
    int v2;

    virtual ~A(){}
};

class B: public A
{
public:
    int v3;
    B(A a)
        : A{std::move(a)}
        , v3{1}
    {}
};

/////////0/////////1/////////2/////////3/////////4/////////5/////////6/////////7
union Space
{
    char stub;
    A a;
    B b;
    Space() : stub{} {}
    ~Space() {}
} space{};

//ну, это и так понятно
static_assert(static_cast<void*>(&space.a) == static_cast<void*>(&space.b));

//важный инвариант: самостоятельная часть A и под-часть A из B - лежат в одном и том же месте
static_assert(&space.a == static_cast<A*>(&space.b));


/////////0/////////1/////////2/////////3/////////4/////////5/////////6/////////7
A* storedPa{};
void store(A* pa)
{
    storedPa = pa;
}

void useStoredPointer()
{
    storedPa->v1 = 220;
    int k = storedPa->v2;
}

/////////0/////////1/////////2/////////3/////////4/////////5/////////6/////////7
int main()
{
    //создаем общий функционал A
    new(&space.a) A;

    store(&space.a);
    useStoredPointer();

    //немного с ним поработали, поняли что его надо заменить на уточненный функционал B
    {
        //запомним общее состояние
        A aState = std::move(space.a);
        //разрушим общий оригинал
        space.a.~A();
        //пересоздадим его как уточненный, заодно зарядим его прошлым состоянием
        new(&space.b) B{std::move(aState)};
    }

    //теперь работаем с B
    //причем так что его общая под-часть A имеет такой же адрес как и раньше до B
    useStoredPointer();

    //поработали, заканчиваем время жизни B
    space.b.~B();

    return 0;
}
Re[7]: placement new
От: sergii.p  
Дата: 11.12.22 09:39
Оценка: +1
Здравствуйте, maks1180, Вы писали:

M>Адрес же cl1 и cc будет отличаться.


ну а так?

std::variant<ClientCommon, Client1, Client2> v;
v = ClientCommon{};
v = Client1{std::move(std::get<ClientCommon>(v))};
Re[3]: placement new
От: vopl Россия  
Дата: 11.12.22 12:45
Оценка: +1
Здравствуйте, maks1180, Вы писали:

M>Спасибо, но я хотел бы обойтись без std::move, тем более без двойного.

M>Ведь мы же создаём объект на том же месте, или конструктор может затереть всё нулями ?

Конструктор не затрет, если он будет тривиальным и нигде не будет инициировано zero-initialization, во всех остальных случаях — затрет. Но это семечки, есть более страшная штука, касается UB по причине использования несконструированных (вне lifetime) значений, тех самых, которые не были "затерты" конструктором и остальсь от предыдущего объекта. Это практически опасная ситуация, потому что компилятор будет чудить по причине этого UB.

Другими словами — не делай так.
Re[5]: placement new
От: rg45 СССР  
Дата: 12.12.22 14:40
Оценка: :)
Здравствуйте, σ, Вы писали:

σ>https://cplusplus.github.io/CWG/issues/2523.html


--
Не можешь достичь желаемого — пожелай достигнутого.
placement new
От: maks1180  
Дата: 10.12.22 17:46
Оценка:
Допустим есть 2 класса:
сlass A {
public: int v1; int v2;
};

class B: public сlass A {
int v3;
B() { v3 = 1; }
};

сначала я создал экземпляр сlass A через placement new
new(p)A();
поработал с ним, потом создал сlass B, на тот же месте где был Class A
new(p)B();

1) Есть ли гарантии, что v1 и v2 класс A и В будут по одинаковым смещениям ?
2) Есть ли гарантии, что конструктор класса B не перетрёт значения v1, v2 которые остались в памяти от прошлого экземпляра класса A ?

Таким образом я хочу динамически изменить класс объекта сохранив его в той же области памяти.
Или может можно как-то поменять v-table у объекта и он станет объектом другого класса ?

Такое изощерение для того что-бы не вызывать epoll_ctl(..., EPOLL_CTL_MOD) при смене адреса, который занимает около 4100 тактов.
===============================================
(реклама, удалена модератором)
Отредактировано 10.12.2022 17:55 maks1180 . Предыдущая версия . Еще …
Отредактировано 10.12.2022 17:53 maks1180 . Предыдущая версия .
Отредактировано 10.12.2022 17:47 maks1180 . Предыдущая версия .
Re: placement new
От: T4r4sB Россия  
Дата: 10.12.22 17:57
Оценка:
Здравствуйте, maks1180, Вы писали:

M>Таким образом я хочу динамически изменить класс объекта сохранив его в той же области памяти.


А можно задачу в общее общем виде? Не ту подзадачу, что ты сформулировал, а вот ту, которую реально надо решить?
Re[2]: placement new
От: maks1180  
Дата: 10.12.22 18:10
Оценка:
TB>А можно задачу в общее общем виде? Не ту подзадачу, что ты сформулировал, а вот ту, которую реально надо решить?

Есть сетевой сервис (приложение):
1) на каждое входящее соединение создаём объект класс ClientCommon и далее он отвечает за работу с этим соединением.
2) после общения с клиентом, мы понимает что он хочет от сервиса и должны создать заместо ClientCommon либо Client1 либо Client2
3) далее уже Client1 либо Client2 будут обрабатывать работу с данным соединением

Если меняется адрес объекта нужно вызвать epoll_ctl(..., EPOLL_CTL_MOD), который занимает около 4100 тактов.
Если сделать через прослойку указателей через std::deque я мерил занимает около 50 тактов, на каждый recv() (поступивший пакет), что может превысить штраф в 4100 тактов который нужно заплатить 1 раз при смене объекта.

Поэтому я решил не менять адрес объекта и не использовать прослойки указателей.
===============================================
(реклама, удалена модератором)
Отредактировано 10.12.2022 18:19 maks1180 . Предыдущая версия . Еще …
Отредактировано 10.12.2022 18:11 maks1180 . Предыдущая версия .
Re[2]: placement new
От: maks1180  
Дата: 10.12.22 18:12
Оценка:
R>Согласно стандарту языка, использование одной и той же области памяти для размещения двух разных объектов порождает неопределенное поведение. Нельзя так делать.

Даже если первый объект больше не будет использован после создания второго ?
===============================================
(реклама, удалена модератором)
Re[3]: placement new
От: T4r4sB Россия  
Дата: 10.12.22 18:18
Оценка:
Здравствуйте, maks1180, Вы писали:

M>Даже если первый объект больше не будет использован после создания второго ?


Компилятор предположит, что эти объекты никак друг к другу не относятся, то есть он может обращение к полю "предыдущего" объекта воспринять как обращение к неинициализированной памяти, например.
Re[3]: placement new
От: T4r4sB Россия  
Дата: 10.12.22 18:20
Оценка:
Здравствуйте, maks1180, Вы писали:

TB>>А можно задачу в общее общем виде? Не ту подзадачу, что ты сформулировал, а вот ту, которую реально надо решить?


M>Есть сетевой сервис (приложение):

M>1) на каждое входящее соединение создаём объект класс ClientCommon и далее он отвечает за работу с этим соединением.
M>2) после общения с клиентом, мы понимает что он хочет от сервиса и должны создать заместо ClientCommon либо Client1 либо Client2
M>3) далее уже Client1 либо Client2 будут обрабатывать работу с данным соединением

Неужели просто смувать результат в соответвующее поле конечного результата настолько дорого?
Re[4]: placement new
От: maks1180  
Дата: 10.12.22 18:22
Оценка:
TB>Неужели просто смувать результат в соответвующее поле конечного результата настолько дорого?

Не понял, что ты имеешь ввиду.
===============================================
(реклама, удалена модератором)
Re[5]: placement new
От: T4r4sB Россия  
Дата: 10.12.22 18:33
Оценка:
Здравствуйте, maks1180, Вы писали:

TB>>Неужели просто смувать результат в соответвующее поле конечного результата настолько дорого?


M>Не понял, что ты имеешь ввиду.

class ClientCommon {...};
class Client1 { ClientCommon  common; ...};
class Client2 { ClientCommon  common; ...};

ClientCommon cc;
foo(cc);
if (condition) {
  Client1 cl1;
  cl1.common = std::move(cc);
  ...
} else {
  Client2 cl2;
  cl2.common = std::move(cc);
  ...
}

Это если не хочется лишнюю индирекцию в коде иметь. Про std::deque не понял — почему если прослойка указателей, то std::deque?
Re[6]: placement new
От: maks1180  
Дата: 10.12.22 18:39
Оценка:
TB>
TB>class ClientCommon {...};
TB>class Client1 { ClientCommon  common; ...};
TB>class Client2 { ClientCommon  common; ...};

TB>ClientCommon cc;
TB>foo(cc);
TB>if (condition) {
TB>  Client1 cl1;
TB>  cl1.common = std::move(cc);
TB>  ...
TB>} else {
TB>  Client2 cl2;
TB>  cl2.common = std::move(cc);
TB>  ...
TB>}
TB>


Адрес же cl1 и cc будет отличаться.

TB>Это если не хочется лишнюю индирекцию в коде иметь. Про std::deque не понял — почему если прослойка указателей, то std::deque?

std::vector тормоза сильные может дать при росте.
===============================================
(реклама, удалена модератором)
Re[7]: placement new
От: T4r4sB Россия  
Дата: 10.12.22 18:46
Оценка:
Здравствуйте, maks1180, Вы писали:

M>Адрес же cl1 и cc будет отличаться.


А там типа этот адрес уже сохранён в 100500 мест, и надо во всех этих местах обновить значение, да?

M>std::vector тормоза сильные может дать при росте.


Ну так он в какой-то момент прекращает расти.
А ещё если у тебя х64 и с маппингом поиграться, то можно сделать бесконечный вектор без реаллокаций.
Re[3]: placement new
От: rg45 СССР  
Дата: 10.12.22 19:19
Оценка:
Здравствуйте, maks1180, Вы писали:

R>>Согласно стандарту языка, использование одной и той же области памяти для размещения двух разных объектов порождает неопределенное поведение. Нельзя так делать.


M>Даже если первый объект больше не будет использован после создания второго ?


А почему это он не будет использован? Время жизни объекта продолжатеся и, как минимум, для него еще будет вызван деструктор (который формально есть даже у тривиальных типов).
--
Не можешь достичь желаемого — пожелай достигнутого.
Re[4]: placement new
От: T4r4sB Россия  
Дата: 10.12.22 19:31
Оценка:
Здравствуйте, rg45, Вы писали:

R> и, как минимум, для него еще будет вызван деструктор


Это после placement new-то?
Re[5]: placement new
От: rg45 СССР  
Дата: 10.12.22 19:39
Оценка:
Здравствуйте, T4r4sB, Вы писали:

R>> и, как минимум, для него еще будет вызван деструктор


TB>Это после placement new-то?


Я не понял, а что, если объект был создан через placement new, то корректно завершать его время жизни не обязательно? Или ты у меня увидел где-то слово "автоматически"? Имелось в виду, что well-formed программа обязана обеспечить корректное завершение времени жизни объекта, тем или иным образом.
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 10.12.2022 19:42 rg45 . Предыдущая версия . Еще …
Отредактировано 10.12.2022 19:41 rg45 . Предыдущая версия .
Re: placement new
От: andyp  
Дата: 10.12.22 23:26
Оценка:
Здравствуйте, maks1180, Вы писали:


M>Таким образом я хочу динамически изменить класс объекта сохранив его в той же области памяти.

M>Или может можно как-то поменять v-table у объекта и он станет объектом другого класса ?

Т.е. нужен юнион, объекты внутри которого хранятся по одному адресу?

Одна опция — замутить свой variant. Ну или взять кусок реализации в libstdc++, которая вроде этому условию удовлетворяет.
Re[2]: placement new
От: maks1180  
Дата: 11.12.22 06:42
Оценка:
S> T * ret = new (&m_data) T(std::forward<T_args>(p_args) ...);

Разве ret может отличатся от &m_data ?

Как показывает мой опыт, добавление промежуточного указателя m_base, может повлечь снижение производительности за счёт промаха кэша.
===============================================
(реклама, удалена модератором)
Отредактировано 11.12.2022 6:51 maks1180 . Предыдущая версия .
Re[2]: placement new
От: maks1180  
Дата: 11.12.22 06:43
Оценка:
M>>Таким образом я хочу динамически изменить класс объекта сохранив его в той же области памяти.
M>>Или может можно как-то поменять v-table у объекта и он станет объектом другого класса ?

A>Т.е. нужен юнион, объекты внутри которого хранятся по одному адресу?


Да. Но нужно как-то поменять v-table у объекта и он станет объектом другого класса ?

A>Одна опция — замутить свой variant. Ну или взять кусок реализации в libstdc++, которая вроде этому условию удовлетворяет.
===============================================
(реклама, удалена модератором)
Re[8]: placement new
От: maks1180  
Дата: 11.12.22 11:29
Оценка:
SP>
SP>std::variant<ClientCommon, Client1, Client2> v;
SP>v = ClientCommon{};
SP>v = Client1{std::move(std::get<ClientCommon>(v))};
SP>


Идея хорошая, только я без std::move хотел обойтись. Если Client1 наследник от ClientCommon, то не нужно дедать std::move, так как он по тому же адресу должен лежать.
Или не так ?
===============================================
(реклама, удалена модератором)
Re[2]: placement new
От: maks1180  
Дата: 11.12.22 11:35
Оценка:
Спасибо, но я хотел бы обойтись без std::move, тем более без двойного.
Ведь мы же создаём объект на том же месте, или конструктор может затереть всё нулями ?
===============================================
(реклама, удалена модератором)
Re[3]: placement new
От: andyp  
Дата: 11.12.22 14:23
Оценка:
Здравствуйте, maks1180, Вы писали:

M>Да. Но нужно как-то поменять v-table у объекта и он станет объектом другого класса ?


Так нельзя. Нарушаешь стандарт на счёт времени жизни. Обязательно нужно конструктор и деструктор звать. Так что, placement new как в variant — единственный правильный путь имхо.
Re[3]: placement new
От: Sm0ke Россия ksi
Дата: 11.12.22 17:24
Оценка:
Здравствуйте, maks1180, Вы писали:

S>> T * ret = new (&m_data) T(std::forward<T_args>(p_args) ...);


M>Разве ret может отличатся от &m_data ?


Вроде нет. Значит можно написать return &m_data; а ret убрать.

M>Как показывает мой опыт, добавление промежуточного указателя m_base, может повлечь снижение производительности за счёт промаха кэша.


m_base может отличаться от &m_data
Re[9]: placement new
От: Sm0ke Россия ksi
Дата: 11.12.22 17:50
Оценка:
Здравствуйте, maks1180, Вы писали:

SP>>
SP>>std::variant<ClientCommon, Client1, Client2> v;
SP>>v = ClientCommon{};
SP>>v = Client1{std::move(std::get<ClientCommon>(v))};
SP>>


M>Идея хорошая, только я без std::move хотел обойтись. Если Client1 наследник от ClientCommon, то не нужно дедать std::move, так как он по тому же адресу должен лежать.

M>Или не так ?

1. Нет гарантий, что это так
2. Нет гарантий, что данные не будут перезатёрты

//
Можно хранить параметры отдельно, и добавить указатель на них в ClientCommon, если не хочешь их мувать.

//
Я понимаю, что ты хочешь досоздать Client1 из ClientCommon, а cpp этого нет.
Но тогда можно сделать pimpl.

struct ClientCommon;

struct ClientImpl { ClientCommon * m_outer; };
struct Client1 : ClientImpl {};
struct Client2 : ClientImpl {};

struct ClientCommon {
  // общие данные
  
  // реализация
  keep<ClientImpl, Client1, Client2> m_impl;
};
Отредактировано 11.12.2022 17:51 Sm0ke . Предыдущая версия .
Re[4]: placement new
От: vopl Россия  
Дата: 11.12.22 18:11
Оценка:
Здравствуйте, vopl, Вы писали:

V>Здравствуйте, maks1180, Вы писали:


M>>Спасибо, но я хотел бы обойтись без std::move, тем более без двойного.

M>>Ведь мы же создаём объект на том же месте, или конструктор может затереть всё нулями ?

V>Конструктор не затрет, если он будет тривиальным и нигде не будет инициировано zero-initialization, во всех остальных случаях — затрет. Но это семечки, есть более страшная штука, касается UB по причине использования несконструированных (вне lifetime) значений, тех самых, которые не были "затерты" конструктором и остальсь от предыдущего объекта. Это практически опасная ситуация, потому что компилятор будет чудить по причине этого UB.


V>Другими словами — не делай так.


Кстати кстати, есть еще вот такая штука https://en.cppreference.com/w/cpp/utility/launder, судя по всему это то что тебе нужно, с ее помощью можно заблокировать упомянутое UB
Re[10]: placement new
От: sergii.p  
Дата: 12.12.22 08:50
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>2. Нет гарантий, что данные не будут перезатёрты


ага
https://godbolt.org/z/e5M49nc7P
но с вариантами всё будет работать. Там создаётся новый вариант и только потом вызывается конструктор перемещения. В общем, два перемещения. Если вызывать emplace то всё будет печально.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.