Может ли копирование elision происходить через synchronize-with с заявлениями?

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

user_type foo()
{
  unique_lock lock( global_mutex );
  return user_type(...);
}

user_type result = foo();

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

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


Чтобы гарантировать, что ответ касается не только специального случая, обратите внимание на то, что копия elision (согласно моему чтению) все еще разрешена, если у меня есть оператор вроде new (&result)( foo() ). То есть, result не должен быть объектом стека. user_type сам может также работать с данными, разделяемыми между потоками.


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

Ответы

Ответ 1

Нити не имеют к этому никакого отношения, но порядок конструкторов/деструкторов блокировки может повлиять на вас.

Глядя на шаги низкого уровня, которые ваш код делает, с отсутствием копирования, один за другим (с использованием опции GCC -fno-elide-constructors):

  • Построить lock.
  • Создайте временные аргументы user_type с (...).
  • Копировать-построить временное возвращаемое значение функции типа user_type, используя значение с шага 2.
  • Уничтожьте временную часть с шага 2.
  • Уничтожьте lock.
  • Скопируйте конструкцию user_type result, используя значение с шага 3.
  • Уничтожьте временную часть с шага 3.
  • Позже уничтожьте result.

Естественно, что при оптимизации оптимизации нескольких копий это будет просто:

  • Построить lock.
  • Создайте объект result непосредственно с помощью (...).
  • Уничтожьте lock.
  • Позже уничтожьте result.

Обратите внимание, что в обоих случаях конструктор user_type с (...) защищен блокировкой. Любой другой конструктор конструктора или деструктор не может быть защищен.

запоздалые мысли

Я думаю, что наиболее вероятное место, где это может вызвать проблемы, находится в деструкторах. То есть, если ваш исходный объект, построенный с помощью (...), обрабатывает любой общий ресурс иначе, чем его копии, и делает что-то в деструкторе, которому требуется блокировка, тогда у вас есть проблема.

Естественно, это означало бы, что ваш объект плохо проектируется в первую очередь, поскольку копии не ведут себя как исходный объект.

Ссылка

В проекте С++ 11, 12.8.31 (аналогичная формулировка без всех "ходов" находится в С++ 98:

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

  • в операторе return в функции с типом возвращаемого класса, когда выражение является именем энергонезависимый автоматический объект (кроме функции или параметра catch-clause) с тем же cvunqualified тип как возвращаемый тип функции, операцию копирования/перемещения можно опустить при построении автоматический объект непосредственно в функции возвращает значение

  • параметр или параметр catch-clause), объем которого не выходит за пределы самого внутреннего (если есть), операция копирования/перемещения из операнда в исключение объект может быть опущен путем создания автоматического объекта непосредственно в объект исключения

  • когда объект временного класса, который не привязан к ссылке, будет скопирован/перемещен к объекту класса с тем же cv-неквалифицированным типом операция копирования/перемещения может быть опущена построение временного объекта непосредственно в цель пропущенной копии/перемещения

  • когда объявление исключения обработчика исключений объявляет объект того же типа (за исключением cv-qualification) в качестве объекта исключения, операцию копирования/перемещения можно опустить, рассматривая объявление исключения как псевдоним для объекта исключения, если значение программы будет не изменяться, кроме выполнения конструкторов и деструкторов для объекта, объявленного объявление исключения.

Точки 1 и 3 сотрудничают в вашем примере, чтобы исключить все копии.

Ответ 2

"Чтобы гарантировать, что ответ касается не только специального случая, обратите внимание на то, что копия elision (согласно моему чтению) все еще разрешена, если у меня есть оператор вроде new (&result)( foo() ). То есть результат не нужен быть объектом стека. user_type сам может также работать с данными, разделяемыми между потоками."

Там rub: если result является общим, у вас есть гонка данных даже без разрешения. Поведение undefined начинается с.