инициализация некопируемого члена (или другого объекта) на месте из фабричной функции

Класс должен иметь допустимый конструктор копирования или перемещения, чтобы любой из этих синтаксисов был допустимым:

C x = factory();
C y( factory() );
C z{ factory() };

В C++03 было довольно часто полагаться на исключение копирования, чтобы компилятор не касался конструктора копирования. У каждого класса есть допустимый конструктор копирования signature независимо от того, существует ли определение.

В C++11 некопируемый тип должен определять C( C const & ) = delete;, что делает любую ссылку на функцию недействительной независимо от использования (то же самое для неперемещаемого типа). (С++ 11 §8.4.3/2). GCC, например, будет жаловаться при попытке вернуть такой объект по значению. Copy elision перестает помогать.

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

C factory() {
    return { arg1, 2, "arg3" }; // calls C::C( whatever ), no copy
}

Изменить. Если есть какие-либо сомнения, этот оператор return анализируется следующим образом:

  1. 6.6.3/2: «Операция возврата со списком инициализации в фигурных скобках инициализирует объект или ссылку, которые должны быть возвращены из функции с помощью инициализации списка копирования (8.5.4) из указанного списка инициализаторов».
  2. 8.5.4/1: «инициализация списка в контексте инициализации копирования называется инициализацией списка копирования». ¶3: «если T является типом класса, учитываются конструкторы. Перечисляются применимые конструкторы, и лучший из них выбирается с помощью разрешения перегрузки (13.3, 13.3.1.7)».

Пусть вас не вводит в заблуждение название copy-list-initialization. 8.5:

13: Форма инициализации (с использованием круглых скобок или =) обычно не имеет значения, но имеет значение, когда инициализатор или инициализируемый объект имеет тип класса; увидеть ниже. Если инициализируемый объект не имеет типа класса, список-выражений в инициализаторе в скобках должен быть одним выражением.

14: Инициализация, которая происходит в форме T x = a;, а также при передаче аргументов, возврате функции, создании исключения (15.1), обработке исключения (15.3) и инициализации агрегатного члена (8.5.1), называется copy- инициализация.

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

Есть отличия: прямая инициализация может вызвать явный конструктор, в отличие от инициализации копированием. Копирование-инициализация инициализирует временный объект и копирует его для инициализации объекта при конвертации.

Спецификация инициализации-списка-копирования для операторов return { list } просто указывает точный эквивалентный синтаксис, который будет temp T = { list };, где = обозначает инициализацию копирования. Это не означает, что вызывается конструктор копирования.

-- Завершить редактирование.


Затем результат функции может быть получен в ссылке rvalue, чтобы предотвратить копирование временного файла в локальный:

C && x = factory(); // applies to other initialization syntax

Вопрос в том, как инициализировать нестатический член из фабричной функции, возвращающей некопируемый, неперемещаемый тип? Трюк со ссылками не работает, потому что элемент ссылки не продлевает время жизни временного элемента.

Обратите внимание: я не рассматриваю агрегатную инициализацию. Речь идет об определении конструктора.


person Potatoswatter    schedule 17.06.2012    source источник
comment
Инициализация списка не является правильным инструментом для копирования объектов. В качестве примера рассмотрим struct CounterExample { }; CounterExample ce; CounterExample ce1{ ce }; /* ill-formed */.   -  person Johannes Schaub - litb    schedule 17.06.2012
comment
@JohannesSchaub-litb Честное слово, но я не говорил делать это сделать, я сказал, что для этого есть предварительное условие.   -  person Potatoswatter    schedule 17.06.2012
comment
Затем я хотел бы сломать утверждение о том, что требуется допустимый конструктор копирования или перемещения. Рассмотрим struct CounterExample { CounterExample(CounterExample const&) = delete; int a; operator int() { return 42; } }; CounterExample factory() { return {}; } CounterExample c{factory()}; /* fine! */   -  person Johannes Schaub - litb    schedule 17.06.2012
comment
@JohannesSchaub-litb На самом деле я надеялся, что вы скажете мне, является ли это дырой в языке. Мы с Люком обсуждали что-то в чате. Кажется, что это почти единственный случай, когда объект возвращаемого значения должен быть сохранен где-то кроме стека, что может иметь последствия для ABI. Но их также можно сохранить, привязав к пространству имен/статическим ссылкам. Поэтому я озадачен, является ли это семантической необходимостью или синтаксическим упущением.   -  person Potatoswatter    schedule 17.06.2012
comment
Я не считаю GCC правильным. Тип является агрегатом, поэтому в соответствии с C++11 он должен быть принят (clang его принимает)   -  person Johannes Schaub - litb    schedule 17.06.2012
comment
Будет ли эта ошибка GCC причиной моей проблемы в этой ситуации? Я могу успешно скомпилировать следующую функцию: T && make_T() { return std::move(T(constructor_argument)); }. И std::move, и && необходимы для его компиляции. Однако я не могу скомпилировать, когда пытаюсь выполнить member_variable(make_T()) или member_variable(std::move(make_T())). Это где GCC неверен? В моем коде используются функции С++ 11, которые clang не поддерживает, поэтому я не могу проверить это таким образом.   -  person David Stone    schedule 13.08.2012
comment
@DavidStone Johannes имеет в виду конкретно типы агрегатов, которые, вероятно, к вам не относятся. Вы используете прямую инициализацию со скобками, а не инициализацию списка с фигурными скобками. Кроме того, насколько я помню, контрпример Йоханнеса применим к локальным переменным, но не к членам.   -  person Potatoswatter    schedule 13.08.2012
comment
@JohannesSchaub-litb: С++ 14 исправил ваш случай с пустой структурой, чтобы он был копией.   -  person Davis Herring    schedule 18.09.2017


Ответы (2)


По вашему основному вопросу:

Вопрос в том, как инициализировать нестатический член из фабричной функции, возвращающей некопируемый, неперемещаемый тип?

Вы не знаете.

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

Другими словами, это:

C c = {...};

Отличается от этого:

C c = [&]() -> C {return {...};}()

У вас есть функция, которая возвращает тип по значению. Он возвращает выражение prvalue типа C. Если вы хотите сохранить это значение, дав ему имя, у вас есть ровно два варианта:

  1. Сохраните его как const& или &&. Это продлит срок службы временного блока до срока службы блока управления. Вы не можете сделать это с переменными-членами; это можно сделать только с автоматическими переменными в функциях.

  2. Скопируйте/переместите его в значение. Вы можете сделать это с помощью переменной-члена, но для этого, очевидно, требуется, чтобы тип был копируемым или перемещаемым.

Это единственные параметры, которые C++ предоставляет вам, если вы хотите сохранить выражение prvalue. Таким образом, вы можете либо сделать тип перемещаемым, либо вернуть только что выделенный указатель в память и сохранить его вместо значения.

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

person Nicol Bolas    schedule 17.06.2012
comment
Пожалуйста, сошлитесь (или иным образом верните) претензию, что я не прав, не просто утверждайте неправильно и двигайтесь дальше. Я уточню этот момент в вопросе, а затем проголосую за него. - person Potatoswatter; 18.06.2012
comment
Я не предлагал использовать вариант 2, я предлагаю C && x = factory(); - person Potatoswatter; 18.06.2012
comment
@Potatoswatter: Верно. Я не понимал этого раньше. Тем не менее, я также хотел бы отметить, что весь этот список инициализации не делает того, что вы сказали: новый синтаксис для выражения намерения вместо того, чтобы полагаться на лазейку. Это просто синтаксический сахар; построение временного объекта на месте произошло бы, если бы это был явный вызов конструктора, а не список инициализации в фигурных скобках. Что еще более важно, это не меняет основную направленность ответа: вы не можете хранить некопируемое, неперемещаемое временное значение как нестатический член класса. Я почищу это, хотя, чтобы прояснить этот момент. - person Nicol Bolas; 18.06.2012
comment
Нет, явный вызов конструктора приведет к построению перемещения. return {x}; ни в коем случае не переводится в return C{x};, спецификация обрабатывает случаи совершенно по-другому. Если вы не согласны, вам нужно будет сослаться на утверждение. Кроме того, вы просто повторяете данные альтернативы и делаете поспешный вывод, что их не осталось. Возможно, это невозможно сделать непосредственно с помощью члена типа C, но вряд ли уместно делать поспешный вывод о том, что проблема неразрешима. - person Potatoswatter; 18.06.2012
comment
@Potatoswatter: я не спешу с выводами; в С++ доступно очень много вариантов. У вас есть значение x, возвращенное функцией; не имеет значения, как вы сгенерировали это xvalue, потому что локальный код не может определить разницу (если не было доступно определение функции). Вы можете либо сохранить это в ссылке (const& или &&), тем самым продлив ее время жизни, либо скопировать/переместить ее в реальный объект. Это все варианты, которые C++ предоставляет вам, точка. Вы можете вернуть указатель и вставить его в unique_ptr, но вы не вернете класс по значению. - person Nicol Bolas; 18.06.2012
comment
@Potatoswatter: Кстати, я только что понял, что копия, о которой вы говорите, — это копия в возвращаемое значение, а не копия, возвращаемая функцией, в значение, в которое ее записывает пользователь. Хорошо, да, юниформ-инициализация устраняет эту необходимость. Но, как я уже отмечал, то, как создается возвращаемое значение, не меняет требований к хранению этого возвращаемого значения. - person Nicol Bolas; 18.06.2012
comment
Объект возвращаемого значения из функции является prvalue, как и любой временный. Я говорил об обеих копиях, поэтому в вопросе они рассматриваются отдельно. Я не собираюсь предлагать больше исправлений здесь. Проще говоря, я не ищу причину, по которой это невозможно сделать, я ищу минимальное обходное решение. Прямо сейчас мне приходит в голову, что фабрика, возвращающая forward_as_tuple, и конструктор в C, принимающий такой кортеж, могут быть решением. Лучше потратить энергию на пробу, чем на поиск стандартной терминологии. - person Potatoswatter; 18.06.2012
comment
@Potatoswatter: Но тогда это не будет функция factory, потому что она не создает объект. Обходные пути означают, что вы каким-то образом меняете параметры проблемы, но никогда не указываете, какие изменения были бы приемлемы в ваших обстоятельствах. Я предложил использовать указатели; это было приемлемо? Ваше решение делает фабричную функцию на самом деле не фабрикой, а вместо этого полагаясь на общедоступный конструктор. Откуда мы должны были знать, что это решение будет приемлемым? Как правило, цель фабрики состоит в том, чтобы гарантировать, что все строительство идет по известным путям. - person Nicol Bolas; 18.06.2012
comment
давайте продолжим это обсуждение в чате - person Nicol Bolas; 18.06.2012
comment
Извините, когда я формулировал вопрос, я надеялся, что такое изменение не понадобится. Я удалил отрицательный голос, так как похоже, что все ошибки исчезли. (Хотя я не уверен, что часть перед тем, как у вас есть функция… полезна.) Может быть, я приму это; это было бы справедливо для вас. Но ради того, чтобы кто-то еще посетил страницу, обходной путь был бы более полезным. - person Potatoswatter; 18.06.2012
comment
Наконец принять это. Я все еще не уверен в том, что нельзя изменить язык, чтобы принудительно исключить возвращаемые значения, но, вероятно, это верно с точки зрения сохранения ABI с возвращаемыми значениями, выделенными вызываемому. Тем не менее, для вызывающей стороны имеет смысл передать указатель на то место, где должно быть построено возвращаемое значение. (Кроме указателя стека.) Было бы неплохо провести обзор ABI… - person Potatoswatter; 11.07.2013

Подобные проблемы были среди основных мотивов для изменения в C++17, разрешающего эти инициализации (и исключить копии из языка, а не просто как оптимизацию).

person Davis Herring    schedule 18.09.2017