Пропустить список инициализации или контейнер, глядя на семантику перемещения?

EDIT:. Прежде чем мы начнем, этот вопрос не касается правильного использования std::initializer_list; это то, что должно быть принято, когда требуется удобный синтаксис. Спасибо, что остался на тему.


С++ 11 вводит std::initializer_list для определения функций, принимающих аргументы braced-init-list.

struct bar {
    bar( char const * );
    bar( int );
} dog( 42 );

fn foo( std::initializer_list< bar > args );

foo( { "blah", 3, dog } );

Синтаксис хорош, но под капотом это неприятно из-за различных проблем:

  • Они не могут быть значимо перемещены. Вышеупомянутая функция должна скопировать dog из списка; это не может быть преобразовано в операцию перемещения или отклонено. Типы Move-only не могут использоваться вообще. (Ну, const_cast действительно будет допустимым обходным путем. Если есть статья об этом, я бы хотел его увидеть.)

  • Нет семантики constexpr. (Это ожидается в С++ 1y. Это небольшая проблема.)

  • const не распространяется, как и везде; initializer_list никогда не const, но его содержимое всегда есть. (Поскольку он не владеет своим содержимым, он не может предоставить доступ для записи к копии, хотя копирование в любом месте редко будет безопасным.)

  • Объект initializer_list не имеет своего хранилища (yikes); его связь с полностью раздельным голой матрицей (yikes), обеспечивающая хранение, является неопределенным (yikes) как отношение ссылки на связанный временной (четверные yikes).

Я верю, что все это будет исправлено в свое время, но на данный момент существует лучшая практика для получения преимуществ без жесткого кодирования до initializer_list? Существует ли какая-либо литература или анализ в отношении непосредственной зависимости от нее?

Очевидным решением является передача по значению стандартного контейнера, такого как std::vector. Как только объекты будут скопированы в него из initializer_list, он будет создан для перемещения по значению, а затем вы можете переместить содержимое. Улучшение будет заключаться в том, чтобы предложить хранилище в стеке. Хорошая библиотека может предложить большинство преимуществ initializer_list, array и vector, даже не используя первый.

Любые ресурсы?

Ответ 1

речь идет о том, что нужно передавать, когда требуется удобный синтаксис.

Если вы хотите удобство размера (то есть: пользователь просто набирает список {} без вызовов функций или слов), тогда вы должны принять все полномочия и ограничения правильного initializer_list. Даже если вы попытаетесь преобразовать его во что-то другое, например, в какую-либо форму array_ref, вам все равно придется иметь между ними посредник initializer_list. Это означает, что вы не можете обойти ни одну из проблем, с которыми вы столкнулись, например, неспособность выйти из них.

Если он проходит через initializer_list, вы должны принять эти ограничения. Поэтому альтернативой является не проходить через initializer_list, а это означает, что вам нужно будет принять какую-либо форму контейнера со специфической семантикой. И альтернативный тип должен быть агрегатом, так что построение альтернативного объекта не будет сталкиваться с одной и той же проблемой.

Итак, вы, вероятно, пытаетесь заставить пользователя создать std::array (или массив языков) и передать это. Ваша функция может иметь форму array_ref, которая может быть построена из любого массива произвольного размера, поэтому функция потребления не ограничивается одним размером.

Однако вы теряете удобство размера:

foo( { "blah", 3, dog } );

против.

foo( std::array<bar, 3>{ "blah", 3, dog } );

Единственный способ избежать многословия здесь состоит в том, чтобы foo принять std::array в качестве параметра. Это означает, что он может принимать только массив определенного фиксированного размера. И вы не могли использовать предложенный С++ 14 dynarray, потому что он будет использовать посредника initializer_list.

В конечном счете, вы не должны использовать единый синтаксис инициализации для прокрутки списков значений. Это для инициализации объектов, а не для передачи списков вещей. std::initializer_list - это класс, единственная цель которого должна использоваться для инициализации определенного объекта из произвольно длинного списка значений одинаковых типов. Он должен служить промежуточным объектом между конструкцией языка (бит-init-list) и конструктором, к которому эти значения должны быть загружены. Это позволяет компилятору знать, чтобы вызвать конкретный конструктор (конструктор initializer_list), если ему задан соответствующий список значений с привязкой к скобкам.

Вот причина, почему класс существует.

Поэтому вы должны использовать класс исключительно для той цели, для которой он был разработан. Класс существует для того, чтобы пометить конструктор как содержащий список значений из списка с привязкой-init. Поэтому вы должны использовать его только для конструкторов, которые принимают такое значение.

Если у вас есть некоторая функция foo, которая действует как посредник между некоторым внутренним типом (который вы не хотите показывать напрямую) и предоставленным пользователем списком значений, тогда вам нужно взять что-то еще в качестве параметра до foo. Что-то, что имеет семантику, которую вы желаете, которую вы можете затем подавать в свой внутренний тип.

Кроме того, у вас, похоже, есть неправильное представление о initializer_list и движении. Вы не можете выходить из initializer_list, но вы, безусловно, можете переместиться в один:

foo( { "blah", 3, std::move(dog) } );

Третья запись во внутреннем массиве dog будет построена по ходу.