一个lambda表达式是否是合法的默认(非类型模板)参数?

43
所有下面的标准参考都指的是N4861(2020年3月布拉格后工作草案/C++20 DIS)

背景

Q&A 捕获无效的lambda表达式是否是结构类型? 强调了某些lambda表达式是否具有关联的闭包类型(文字和)结构类型的潜在规范不足。这种分类的影响可以允许或拒绝将闭包类型用作非类型模板参数;从本质上讲,将结构类型的lambda表达式作为非类型模板参数传递。

template<auto v>
constexpr auto identity_v = v;

constexpr auto l1 = [](){};
constexpr auto l2 = identity_v<l1>;
现在,根据[expr.prim.lambda.closure]/1,每个lambda表达式的类型是独一无二的。

[...] 一个独一无二的、无名的非联合类类型,称为闭包类型 [...]

另一方面,[basic.def.odr]/1(摘录,重点是我的)指出:

任何翻译单元都不得包含超过一个定义的变量、函数、类类型、枚举类型、模板、参数的默认参数(对于给定作用域中的函数),或默认模板参数

可以说默认模板参数被认为是需要遵守ODR的定义。

问题

这就引出了我的问题:
一个lambda表达式是否是一个合法的默认(非类型模板)参数?如果是的话,这是否意味着每个使用这样一个默认参数的实例化都会实例化一个独特的特化版本?
(如果超出单个实例化会导致ODR违规,请也进行标注)。
为什么?
如果这确实是合法的,每次调用带有lambda作为默认参数的变量模板都会导致实例化一个独特的特化版本。
template<auto l = [](){}>
               // ^^^^^^ - lambda-expression as default argument
constexpr auto default_lambda = l;

static_assert(!std::is_same_v<
    decltype(default_lambda<>),
    decltype(default_lambda<>)>);

GCC(DEMO)和Clang(DEMO)都接受上述程序。
如果编译器正确地接受了这个例子,这意味着允许另一种机制来捕获和检索元编程状态,这种技术在CWG开放问题2118中长期被认为是

... 难以理解并且应该被视为无效。


我相信这个动议解决了这个问题,但我几乎不理解标准措辞。 - metalfox
1
我认为,因为没有捕获的lambda现在是像没有成员的结构体一样的平凡类型,所以可以像它们一样将lambda作为模板参数传递。我不确定可能会发生ODR违规。这些可能会导致模板的多个实例化,而只有一个实例化是预期的。 - cmdLP
1
嗯,模板只能在允许 lambda 表达式仅捕获无内容的范围内声明,因此不会发生任何“神秘”的事情? - Swift - Friday Pie
1
@AlexGuteniev 我认为不将它们设为唯一的会给编译器实现者以及普通的C++开发人员带来ODR问题,但我同意这种设计的副作用可能会令人惊讶。 - dfrib
1
@Swift-FridayPie 我想我可以把它带到其中一个反射器那里,看看是否有人能够提供一个清晰的答案。正如David Herring在链接的Q&A中指出的那样,可能没有任何实际的标准保证捕获无效的lambda是结构类型。 - dfrib
显示剩余14条评论
2个回答

2
不,它不是一个合法的默认非类型模板参数,因为lambda的闭包类型根据CWG 2542的解决方案,不是结构类型。

2542. Is a closure type a structural type?

Consider:

template <auto V>
void foo() {}

void bar() {
  foo<[i = 3] { return i; }>();
}

It is unclear whether the data members of a closure type are public or private. This makes a difference, since it affects whether a closure type is a structural type or not [...]

[...]

Proposed resolution (approved by CWG 2023-03-30):

Change in 7.5.5.2 [expr.prim.lambda.closure] paragraph 2 as follows:

... The closure type is not an aggregate type (9.4.2 [dcl.init.aggr]) and not a structural type (13.2 [temp.param]). ...


说实话,DR的动机有点站不住脚。而且他们决定违背现有的实施方式,这也非常奇怪,考虑到结构类型是以一种可行的方式定义的。 - undefined

0

lambda表达式是否是合法的默认(非类型)模板参数,如果是的话,这是否意味着每个使用此默认参数的实例化都会实例化一个唯一的专门实例?

是的,这意味着给定你的模板变量:

template<auto l = [](){}>
constexpr auto default_lambda = l;

每次调用 default_lambda 都会生成一个新的唯一闭包类型,从而产生一个新的模板实例,并提供一种捕获元编程状态的新方法。
因此,使用 default_lambda 的每个表达式都必须重新评估。如果编译器的状态自上次实例化以来发生了变化,并且我们在依赖表达式中使用它(例如:检查类型是否定义),则表达式的结果可能会改变。 例如:
struct X; // not defined

template <typename T, auto = default_lambda<>>
consteval bool is_defined() {
    if constexpr (requires { T{}; }) {
        return true;
    } else {
        return false;
    }
}

static_assert(is_defined<X>() == false);

struct X {};
static_assert(is_defined<X>() == true);

1
虽然consteval示例很有趣,但这只是重复了问题中已经观察到的:编译器似乎将每个实例视为唯一的专业化。我所寻找的,强调了我的“语言律师”标签的问题,是提供一个明确答案的规范参考。对于这些类型的异常主题,编译器实现的可能并不是标准所说的,观察到的行为可能仅仅是巧合,甚至表现良好但格式不正确的NDR。 - dfrib
你引用的部分似乎表明这种行为是规范的。 - Mechap

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接