函数模板中定义的 lambda 闭包类型有什么特点?

3

我知道Lambda表达式的类型是“隐藏”的和独特的。但是“独特”具体意味着什么?特别地,相同的Lambda代码是否会给出相同的类型?

(如果可能的话)我想在类型检查中使用这个“不变量”,就像这里所示的那样:

#include <type_traits>

template <typename T>
auto foo(const T y)
{
  return [=](T x) { return x * y; }; 
}

int main()
{
  auto f1 = foo<int>(4.);
  auto f2 = foo<int>(6.); // fails with auto f2 = foo<double>(6.);

  static_assert(std::is_same_v<decltype(f1), 
                               decltype(f2)>); // is this ok or undefined behavior?
}

在此之前,我想确保一下C++标准所说的内容:这样做可以还是会导致未定义的行为?
(这段代码可以成功编译使用g++和clang++,但只有符合标准的内容才重要)
作为更符合初始标题的补充(很抱歉来晚了,因为我不得不离开办公室),这里有一个更严格的版本,它无法 编译:
#include <type_traits>

template <typename T>
auto foo_1(const T y)
{
  return [=](T x) { return x * y; };
}

template <typename T>
auto foo_2(const T y)
{
  return [=](T x) { return x * y; };
}

int main()
{
  auto f1 = foo_1<int>(4.);
  auto f2 = foo_2<int>(6.);

  static_assert(std::is_same_v<decltype(f1), decltype(f2)>); // fails!
}

2
嗯,看起来 foo<int> 是一个函数,而函数有特定的返回类型。因此,它返回的每个对象都需要具有相同的类型。我怀疑由于这个原因,f1f2 必须具有相同的类型。 - François Andrieux
@FrançoisAndrieux,这是一个有趣的评论,谢谢。 - Picaud Vincent
1
就其价值而言,在这里是标准关于闭包类型的相关内容。 - François Andrieux
2
在我看来,标题与问题主体不符。您没有相同的lambda表达式,而是只有一个lambda(表达式[...](...){...}仅出现一次)(更准确地说,每个模板实例化都有一个,但这里只有一个实例化)。 - HolyBlackCat
如果你将模板更改为 template <typename T, int DUMMY>,并使用 auto f1 = foo<int, 1>(4.);auto f2 = foo<int, 2>(6.); ... 这是否有所启示?还是与问题无关? - Eljay
显示剩余6条评论
2个回答

6

虽然标准并没有明确说明“唯一”是什么意思,但我认为我们可以弄清楚。

首先,请注意标准中的“unique, unnamed non-union class type”。形容词“unnamed”足以暗示lambda闭包类型永远不同于使用structclass关键字或标准库定义的某些命名类(例如,lambda闭包类型不能是std::function<...>)。

那么,为什么标准要说“unique, unnamed”呢?这必须是在告诉我们两个不同的lambda闭包类型之间的比较。尽管如此,在许多情况下,这将是多余的。如果我们有两个lambda表达式:

auto f1 = [](int x) { return x; };
auto f2 = [](int x) { return 2*x; };

很明显,这两个lambda表达式不能拥有相同的类型,因为它们的函数调用运算符具有不同的行为。因此,它们各自的闭包类型是“独特的”这一陈述并不会告诉我们任何关于f1f2的新信息。

然而,如果我们现在看:

auto f3 = [](int x) { return x; };
auto f4 = [](int x) { return x; };

在这里,“unique”可以理解为说f3和f4肯定具有不同的类型,尽管lambda表达式拥有相同的拼写和行为。
这通常被认为是在此上下文中“unique”的含义。如果标准没有意味着说f3和f4具有不同的类型,那么根本没有使用“unique”一词的原因。相反,它将只说“未命名”,而不是“唯一未命名”。
然后问题就出现了,独特性能够延伸到多远。它可能意味着每次对lambda表达式的评估都会产生不同的类型吗?不能这样做。那将使表达式的静态类型取决于它执行了多少次(一个运行时属性),这是荒谬的。考虑一下,如果您的foo的lambda每次foo被执行时都具有不同的类型,会发生什么。这将意味着foo的签名每次调用时都会更改。这是不可能的。foo中的lambda必须始终具有相同的类型。
至于你的foo_1和foo_2情况,那里的两种lambda类型必须不同。同样,如果不是这样,那么“unique”一词将是多余的。

我的好夏洛克,你的推断非常棒,不过我可能允许自己在这个完整的程序中获得我需要关于同一主题的所有答案。-- https://wandbox.org/permlink/V3TJb20cKjgY2Bi2 - Chef Gladiator
“不,它绝对不能意味着那个。” - 我认为我们也可以围绕ODR建立一个论点?但我想结论只能是“如果这是真的,你永远无法使用lambda”,这在理论上并没有被排除。 ;) - Yakk - Adam Nevraumont

4

你所做的断言不是未定义行为。闭包类型是实现定义的。一旦在同一个编译器中比较lambda表达式类型,就没有问题了。


1
这真的是实现定义吗?我认为只有标准规定为实现定义的事物才能被视为实现定义。我在闭包类型部分(7.5.5.1)中没有看到这一点,所以我不确定该术语是否适用。除非我错过了或者误解了实现定义行为的性质。另外,实现定义行为需要由实现文档记录。我不知道是否有任何实现记录他们使用的闭包类型。 - François Andrieux
1
@FrançoisAndrieux 它被称为具有定义属性的“未命名”类型。根据标准,它不是实现定义的。 - SergeyA
1
我同意这不是未定义行为;但你还没有证明它是实现定义的。 - Yakk - Adam Nevraumont
@Yakk-AdamNevraumont,我敢大胆地猜测,如果在每个编译器中都重复这个操作,输出将足够不同,以便让人声称“实现定义”-- https://wandbox.org/permlink/V3TJb20cKjgY2Bi2 - Chef Gladiator
这里有什么疑问吗? - Chef Gladiator
显示剩余2条评论

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