c++ 不同的Lambda函数可以衰减为相同的函数指针吗?

jogvjijk  于 2023-07-01  发布在  其他
关注(0)|答案(2)|浏览(126)

两个不同的lambda表达式(没有捕获,相同的args和body)可以衰减到相同的函数指针吗?
我有这个代码:

#include <cassert>
#include <type_traits>

int main() {
    auto f0 = [](auto x) { return x; };
    auto f1 = [](auto x) { return x; };
    
    static_assert(not std::is_same_v<decltype(f0), decltype(f1)>);

    // MSVC Release-mode combines the functions so the pointers are the same (even though the types should be different.
    assert(static_cast<int(*)(int)>(f0) != static_cast<int(*)(int)>(f1));
}

https://godbolt.org/z/P3vc45654
我相信static_assert肯定能通过。assert能保证通过吗?(我看到MSVC在发布模式下无法在我的计算机上运行assert

cedebl8k

cedebl8k1#

我不同意现有的答案。
您没有使用函数调用运算符。你使用的是“指向函数的指针”的转换。因为lambda表达式有auto参数,所以它们是泛型lambda表达式。
在这种情况下,向“函数指针”的转换描述如下(N4950,[expr.prim.lambda.closure]/9):
对于没有lambda捕获的泛型lambda,闭包类型有一个转换函数模板指向函数指针。转换函数模板具有与函数调用操作符模板相同的发明模板参数列表,并且函数指针具有相同的参数类型。指向函数的指针的返回类型应表现得好像它是一个decltype-specifier,表示相应函数调用运算符模板专门化的返回类型。
这里没有提到创建指向唯一函数的指针,或者类似的东西。
微软的实现似乎没有违反这里的任何规则。

35g0bw71

35g0bw712#

我认为这个问题更多地与Visual Studio构建过程的特性有关,因为Visual Studio编译器中的常量表达式检查正确地证明了它认为两个函数指针是不同的:

constexpr auto p0 = static_cast<int(*)(int)>(f0);
    constexpr auto p1 = static_cast<int(*)(int)>(f1);
    // passes in all compilers, including MSVC
    static_assert( p0 != p1 );

在线演示:https://gcc.godbolt.org/z/Msb3zTPjz
请注意,同样的地址问题不仅可以在泛型lambda中观察到,而且可以在普通闭包对象和简单的普通函数中观察到。在最简化的形式中,它可以表示如下:

void f0() {}
void f1() {}

void(*p0)();
void(*p1)();

int main() {
    p0 = f0;
    p1 = f1;

    // returns 1 in GCC and Clang, and MSVC debug mode
    // returns 0 in MSVC release mode
    return( p0 != p1 );
}

Visual Studio生成的程序集实际上是正确的,因为编译器真正比较了函数指针是否相等:

void (__cdecl* p0)(void) DQ 01H DUP (?)                     ; p0
void (__cdecl* p1)(void) DQ 01H DUP (?)                     ; p1

void f0(void) PROC                               ; f0, COMDAT
        ret     0
void f0(void) ENDP                               ; f0

void f1(void) PROC                               ; f1, COMDAT
        ret     0
void f1(void) ENDP                               ; f1

main    PROC                                            ; COMDAT
        lea     rdx, OFFSET FLAT:void f0(void)             ; f0
        xor     eax, eax
        lea     rcx, OFFSET FLAT:void f1(void)             ; f1
        mov     QWORD PTR void (__cdecl* p0)(void), rdx       ; p0
        cmp     rdx, rcx
        mov     QWORD PTR void (__cdecl* p1)(void), rcx       ; p1
        setne   al
        ret     0
main    ENDP

在线演示:https://gcc.godbolt.org/z/Mc5qnKzx3
正是 linker 将两个函数合并为一个函数,因为在发布版本中默认启用了/OPT:ICF选项。
manual中有如下警告:
因为/OPT:ICF会导致相同的地址被分配给不同的函数或只读数据成员(即使用/戈伊编译时的常量变量),所以它会中断依赖于函数或只读数据成员的唯一地址的程序。有关详细信息,请参见/戈伊(启用函数级链接)。
因此,可以得出结论,这种优化是有用的,但可能会破坏一些有效的C程序。实际上,它不符合C标准[expr.eq/3.2]
比较指针定义如下:...

  • 否则,如果两个指针都为空,都指向同一个函数,或者都表示同一个地址,则它们比较相等。
  • 否则,指针比较不相等。

因为它们不指向同一个函数,所以指针必须比较不相等。

相关问题