Может ли использование lambda в файлах заголовков нарушать ODR?

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

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

или

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

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

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

Ответ 1

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

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

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

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

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

Типы должны быть одинаковыми.

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

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

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

Таким образом, Lambdas внутри таких тел функций безопасен, так как определение типа замыкания действительно находится в области блока ([expr.prim.lambda]/3 ). < ш > Следовательно, несколько определений f всегда были четко определены.

Эта резолюция, конечно же, не охватывает все сценарии, так как существует много других типов сущностей с внешней связью, которые могут использовать lambdas, в частности, шаблоны функций - это должно быть охвачено другой основной проблемой.
Тем временем 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) в приведенной выше цитате.

Ответ 2

ОБНОВЛЕНО

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

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

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