Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can Different Lambdas Decay to the Same Function Pointer?

Can two different lambdas (that have no capture, and the same args and body) decay to the same function pointer?

I have this code:

#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

I believe the static_assert is guaranteed to pass. Is that assert guaranteed to pass? (I'm seeing MSVC in release mode failing the assert on my computers.)

like image 762
Ben Avatar asked Sep 07 '25 14:09

Ben


2 Answers

I have to disagree with the existing answers.

You're not using the function call operator. You're using the conversion to "pointer to function". Since your lambdas have auto parameters, they're generic lambdas.

The conversion to "pointer to function" in this case is described as follows (N4950, [expr.prim.lambda.closure]/9):

For a generic lambda with no lambda-capture, the closure type has a conversion function template to pointer to function. The conversion function template has the same invented template parameter list, and the pointer to function has the same parameter types, as the function call operator template. The return type of the pointer to function shall behave as if it were a decltype-specifier denoting the return type of the corresponding function call operator template specialization.

Nothing here says anything about creating pointers to unique functions, or anything similar.

Microsoft's implementation does not seem to violate any rules here.

like image 126
Jerry Coffin Avatar answered Sep 10 '25 07:09

Jerry Coffin


I think that this question is related more to Visual Studio build process peculiarities, because constant expression check in Visual Studio compiler correctly proves that it considers two function pointers as distinct:

    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 );

Online demo: https://gcc.godbolt.org/z/Msb3zTPjz

Please note that same address issue can be observed not only with generic lambdas, but also with ordinary closure objects and simply with plain functions. In the most reduced form it can be presented as follows:

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 );
}

The assembly that Visual Studio produces is actually correct in the sense that the compiler truly compares function pointers on equality:

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

Online demo: https://gcc.godbolt.org/z/Mc5qnKzx3

It is the linker that combines two functions into one due to the option /OPT:ICF enabled by default in Release builds.

There is the warning as follows in the manual:

Because /OPT:ICF can cause the same address to be assigned to different functions or read-only data members (that is, const variables when compiled by using /Gy), it can break a program that depends on unique addresses for functions or read-only data members. For more information, see /Gy (Enable Function-Level Linking).

So one may conclude that this optimization is useful, but can break some valid C++ programs. And indeed it is not compliant with C++ standard [expr.eq/3.2] saying

Comparing pointers is defined as follows: ...

  • Otherwise, if the pointers are both null, both point to the same function, or both represent the same address, they compare equal.
  • Otherwise, the pointers compare unequal.

Since they point not on the same function, the pointers must compare unequal.

like image 43
Fedor Avatar answered Sep 10 '25 06:09

Fedor