std::vector инициализация перемещения / копирования конструктора элемента

У меня есть этот кусок кода:

#include <iostream>
#include <vector>

using namespace std;

class Foo{
public:
    Foo() noexcept {cout << "ctor" << endl;}
    Foo(const Foo&) noexcept {cout << "copy ctor" << endl;}
    Foo(Foo&&) noexcept {cout << "move ctor" << endl;}

    Foo& operator=(Foo&&) noexcept {cout << "move assn" << endl; return *this;}
    Foo& operator=(const Foo&) noexcept {cout << "copy assn" << endl; return *this;}

    ~Foo() noexcept {cout << "dtor" << endl;}
};


int main()
{   
    Foo foo;

    vector<Foo> v;
    v.push_back(std::move(foo)); 

    // comment the above 2 lines and replace by
    // vector<Foo> v{std::move(foo)}; 
}

Результат - то, что я ожидаю (скомпилировано с g++ -std=c++11 --no-elide-constructorsТот же вывод без флага)

ctor
move ctor
dtor
dtor

Теперь вместо использования push_back инициализировать непосредственно вектор v как

vector<Foo> v{std::move(foo)};

Я не понимаю, почему я получаю выводы:

1) (без --no-elide-constructors)

ctor
move ctor
copy ctor
dtor
dtor
dtor

2) (с --no-elide-constructors)

ctor
move ctor
move ctor
copy ctor
dtor
dtor
dtor
dtor

В первом случае, почему вызывается копия ctor? И во втором случае, когда компилятор не выполняет elision, я абсолютно не понимаю, почему ctor-код запускается дважды. Есть идеи?

2 ответа

Решение
vector<Foo> v{std::move(foo)};

Здесь вы вызываете конструктор вектора, который принимает std::initializer_list, Список инициализатора позволяет только const доступ к его элементам, поэтому vector придется скопировать каждый элемент из initializer_list в собственное хранилище. Вот что вызывает вызов конструктора копирования.

Из §8.5.4/5 [dcl.init.list]

Объект типа std::initializer_list<E> построен из списка инициализатора, как если бы реализация выделяла временный массив Nэлементы типаconst E, где N это количество элементов в списке инициализатора.


Что касается дополнительного вызова конструктора перемещения с -fno-elide-constructorsЭто обсуждалось в другом ответе пару дней назад. Кажется, что g++ использует очень буквальный подход к реализации примера initializer_list показано в стандарте в том же разделе, который я цитировал выше.

Тот же пример, когда он скомпилирован с использованием clang, не вызывает дополнительный вызов конструктора move.

Контейнеры очень стараются, чтобы они оставались пригодными для использования в случае возникновения исключения. Как часть этого, они будут использовать только std::move внутренне, если конструктор перемещения вашего класса безопасен для исключений. Если это не так (или не может сказать), оно будет скопировано просто для безопасности.

Правильные операции перемещения

Foo(Foo&&) noexcept {cout << "move ctor" << endl;}
Foo& operator=(Foo&&) noexcept {cout << "move assn" << endl;}
Другие вопросы по тегам