Может ли использование лямбды в заголовочных файлах нарушать ODR?

Можно ли в заголовочном файле записать следующее:

inline void f () { std::function<void ()> func = [] {}; }

или же

class C { std::function<void ()> func = [] {}; C () {} };

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

Является ли это нарушением ODR ( One Definition Rule), несмотря на то, что выглядело как обычная модель и разумная вещь? Второй образец нарушает ODR каждый раз или только если хотя бы один конструктор находится в заголовочном файле?

2 ответа

Решение

Это сводится к тому, отличается ли тип лямбды в разных единицах перевода. Если это так, это может повлиять на вывод аргументов шаблона и потенциально может вызвать различные функции - в том смысле, что они должны быть согласованными определениями. Это нарушило бы ODR (см. Ниже).

Однако это не предназначено. Фактически, эта проблема уже была затронута недавно основной проблемой 765, которая конкретно называет встроенные функции с внешними связями - такими как f:

7.1.2 В параграфе 4 [dcl.fct.spec] указано, что локальные статические переменные и строковые литералы, появляющиеся в теле встроенной функции с внешней связью, должны быть одинаковыми сущностями в каждой единице перевода в программе. Однако ничего не сказано о том, должны ли локальные типы быть одинаковыми.

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

Примечания от июльской встречи 2009 года:

Типы предназначены для того же.

Теперь в резолюцию включена следующая формулировка в [dcl.fct.spec] / 4:

Тип, определенный в теле extern inline Функция имеет одинаковый тип в каждой единице перевода.

(Примечание: MSVC пока не относится к вышеприведенной формулировке, хотя это может произойти в следующем выпуске).

Поэтому лямбды внутри тел таких функций безопасны, поскольку определение типа замыкания действительно находится в области видимости блока ( [expr.prim.lambda] / 3).
Отсюда множество определений f были всегда четко определены.

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


Далее следует стандарт, почему различные типы замыканий являются нарушением ODR. Рассмотрим пункты (6.2) и (6.4) в [basic.def.odr] / 6:

Может быть более одного определения […]. Учитывая такой объект по имени D определяется более чем в одной единице перевода, то каждое определение D должен состоять из одинаковой последовательности токенов; а также

(6.2) - в каждом определении D соответствующие имена, просмотренные в соответствии с [basic.lookup], должны ссылаться на сущность, определенную в определении D или должен ссылаться на один и тот же объект после разрешения перегрузки ([over.match]) и после сопоставления частичной специализации шаблона ([temp.over]), […]; а также

(6.4) - в каждом определении D упомянутые перегруженные операторы, неявные вызовы функций преобразования, конструкторов, новых функций оператора и функций удаления оператора, должны ссылаться на одну и ту же функцию или на функцию, определенную в определении D; [...]

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

В вашем примере с C тип замыкания определен внутри класса (область действия которого является наименьшей включающей). Если тип замыкания отличается в двух TU, что стандарт может непреднамеренно подразумевать с уникальностью типа замыкания, конструктор создает экземпляры и вызывает различные специализации function шаблон конструктора, нарушающий (6.4) в приведенной выше цитате.

ОБНОВЛЕНО

В конце концов я согласен с ответом @Columbo, но хочу добавить практические пять центов:)

Хотя нарушение ODR звучит опасно, в данном конкретном случае это не является серьезной проблемой. Лямбда-классы, созданные в разных TU, эквивалентны, за исключением их typeide. Поэтому, если вам не нужно справиться с typeid лямбды, определенной в заголовке (или типом, зависящим от lambda), вы в безопасности.

Теперь, когда нарушение ODR сообщается как ошибка, есть большая вероятность, что оно будет исправлено в компиляторах, которые имеют проблему, например, MSVC и, возможно, в некоторых других, которые не следуют за Itanium ABI. Обратите внимание, что компиляторы, совместимые с Itanium ABI (например, gcc и clang), уже производят ODR-правильный код для лямбд, определенных в заголовке.

Другие вопросы по тегам