Как переместить элементы initializer_list?

Допустим, у вас есть переменная типа std::vector<std::string> и вы инициализируете его списком инициализаторов:

using V = std::vector<std::string>;
V v = { "Hello", "little", "world", "of", "move", "semantics" };

Компилятор создаст временный std::string для каждого строкового литерала создайте список инициализаторов поверх них и затем вызовите ctor для V и создайте вектор. Ктор не знает, что все эти строки являются временными, поэтому он копирует каждую строку.

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

Я что-то упустил или использование списков инициализаторов приводит к ненужным копиям? Я пишу классы, где эта проблема может привести к значительной неэффективности кода. Любая техника, чтобы избежать ненужных копий будет принята с благодарностью.

3 ответа

Решение

Нет способа избежать копирования с initializer_list<string> поскольку стандарт определяет вызов конструктора, принимающего аргумент списка инициализатора, из инициализатора фигурных скобок в качестве фактического аргумента следующим образом (выделение добавлено):

C++ 14 §8.5.4 / 5

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

ИМХО это действительно неудачно.

Обходной путь (для ваших собственных классов) - принять initializer_list<char const*>,


Вот пример обходного пути, примененного к std::vector<string>, Для этого, когда вы не управляете кодом класса, это включает объявление массива данных (на самом деле initializer_list) явно. Это так же, как в C++03, которого механизм списков инициализаторов должен был избежать:

#include <vector>
#include <initializer_list>
#include <iostream>
#include <iterator>              // std::begin, std::end
using namespace std;

struct My_string
{
    char const* const ps;

    My_string( char const* const s )
        : ps( s )
    {
        cout << "  My_string(*) <- '" << s << "'" << endl;
    }

    My_string( My_string const& other )
        : ps( other.ps )
    {
        cout << "  My_string(const&) <- '" << other.ps << "'" << endl;
    };

    My_string( My_string&& other )
        : ps( other.ps )
    {
        cout << "  My_string(&&) <- '" << other.ps << "'" << endl;
    };
};

auto main() -> int
{
    cout << "Making vector a." << endl;
    vector<My_string> const a   = {"a1", "a2", "a3"};
    cout << "Making data for vector b." << endl;
    auto const b_data           = { "b1", "b2", "b3" };
    cout << "Making vector b." << endl;
    vector<My_string> const b( begin( b_data ), end( b_data ) );
}

Выход:

Создание вектора а.
  My_string(*) <- 'a1'
  My_string (*) <- 'a2'
  My_string (*) <- 'a3'
  My_string (const &) <- 'a1'
  My_string (const &) <- 'a2'
  My_string (const &) <- 'a3'
Создание данных для вектора б.
Создание вектора б.
  My_string(*) <- 'b1'
  My_string (*) <- 'b2'
  My_string (*) <- 'b3'

Подумав, я нашел решение, основанное на mutable, Другой ответ по-прежнему в основном правильный, но можно создать прокси с изменяемым членом, чтобы избавиться от верхнего уровня. constи затем переместить элементы оттуда. Поэтому методы, использующие список инициализаторов, должны перегружаться для списка инициализаторов const-ref и версии rvalue-ref, чтобы знать, когда им разрешено перемещаться.

Вот рабочий пример, поначалу это может выглядеть произвольно, но в моем реальном случае использования это решило проблему.

#include <iostream>
#include <vector>

// to show which operations are called
struct my_string
{
    const char* s_;
    my_string( const char* s ) : s_( s ) { std::cout << "my_string(const char*) " << s_ << std::endl; }
    my_string( const my_string& m ) : s_( m.s_ ) { std::cout << "my_string(const my_string&) " << s_ << std::endl; }
    my_string( my_string&& m ) noexcept : s_( m.s_ ) { std::cout << "my_string(my_string&&) " << s_ << std::endl; }
    ~my_string() { std::cout << "~my_string() " << s_ << std::endl; }
};

// the proxy
struct my_string_proxy
{
    mutable my_string s_;

    // add all ctors needed to initialize my_string
    my_string_proxy( const char* s ) : s_( s ) {}
};

// functions/methods should be overloaded
// for the initializer list versions

void insert( std::vector<my_string>& v, const std::initializer_list<my_string_proxy>& il )
{
    for( auto& e : il ) {
        v.push_back( e.s_ );
    }
}

void insert( std::vector<my_string>& v, std::initializer_list<my_string_proxy>&& il )
{
    for( auto& e : il ) {
        v.push_back( std::move( e.s_ ) );
    }
}

int main()
{
    std::vector<my_string> words;
    insert( words, { {"Hello"}, {"initializer"}, {"with"}, {"move"}, {"support"} } );
}

Живой пример

Вместо использования списка инициализаторов за стоимость шаблона вы можете взять ссылку rvalue на массив:

      template<typename T, std::size_t n>
void insert(std::vector<T>& vector, T(&&elements)[n])
{
    for (T& element : elements)
        vector.push_back(std::move(element));
}

(Параметр шаблонаTдля техники не обязательно, если вы знаете тип, ноnявляется.)

      int main()
{
    std::vector<std::unique_ptr<std::string>> strings;
    // C++14 onward: use std::make_unique
    insert(strings, {
        std::unique_ptr<std::string>{ new std::string{} },
        std::unique_ptr<std::string>{ new std::string{"abc"} }
    });
}
Другие вопросы по тегам