27

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

reformed
  • 4,505
  • 11
  • 62
  • 88
Ben
  • 9,184
  • 1
  • 43
  • 56
  • 3
    In Visual Studio release mode, `static_assert` says that two function pointers are distinct, but runtime comparison says that two pointers are the same: https://gcc.godbolt.org/z/Mje1rKz6c Is it allowed? – Fedor Jun 07 '23 at 16:29
  • @Fedor Is what allowed? That the two pointers point to the same address? That seems to me to be a sensible optimisation for the compiler to perform, and doesn't relate to the _type_ of the pointers in any way. – Paul Sanders Jun 13 '23 at 20:01
  • @PaulSanders the thing that seems like it must be a bug in either MSVC or the standard is that `assert(x)` and `static_assert(x)` should always agree. I certainly have code where I `static_assert(x)` then go on to assume `x` at runtime! – Ben Jun 13 '23 at 22:22
  • In this case (and one can probably dream up others) they are checking different things. The `static_assert` is comparing the _types_ of the two lambdas, which differ, as they should do. The `assert` is comparing the addresses of the two synthesised functions and there's no rule that says they cannot be the same. And your results indicate that the compiler has recognised that the two function bodies are identical and only generated one copy of the code, which is impressive. – Paul Sanders Jun 13 '23 at 22:26
  • 3
    I think relevant: https://stackoverflow.com/a/26535126/1708801 – Shafik Yaghmour Jun 13 '23 at 22:32
  • 2
    @PaulSanders, I think it is not allowed. Constant expression comparison works correctly in Visual Studio, but runtime-comparison with /OPT:ICF linker setting deviates from the standard. – Fedor Jun 14 '23 at 06:58
  • 2
    @Fedor I'm coming to the view that you're right. – Paul Sanders Jun 14 '23 at 13:32

2 Answers2

26

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.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • 5
    My thoughts exactly. The pointer just has to point to a function that does what the lambda does. If two lambdas do the same thing then both could point to the same synthesized function. – NathanOliver Jun 06 '23 at 22:40
  • 1
    Prefect. What about if it were `[](int x) { return x; }`? – Ben Jun 07 '23 at 13:54
  • Also, what about two identically-spelled templated functions each cast to the same function-pointer type? As in `template T f0(T x) { return x; } template T f1(T x) { return x; }`: Can it be that `static_cast(f0) == static_cast(f1)`? – Ben Jun 07 '23 at 14:38
  • 2
    Please note that Visual Studio reports that function pointers are the same only if compare them in runtime, `static_assert` still says that they are distinct: https://gcc.godbolt.org/z/Mje1rKz6c Is it legal? – Fedor Jun 07 '23 at 16:41
  • @Ben: For non-generic lambdas it's a little more complex. If the function call operator is not a static member function, then it's pretty much like a generic lambda--has to return the address of *some* function that does the right things, but no requirement for uniqueness. IF it's a static member function, however, the conversion has to return the address of that function (which I think probably does need to be unique per lambda). – Jerry Coffin Jun 07 '23 at 17:19
  • 1
    @Fedor: That's an interesting question, to which I don't have an immediate answer. – Jerry Coffin Jun 07 '23 at 17:19
  • @Ben: As far as function templates with separate names goes....I'd have to look to be sure, but I doubt they can legally be merged. The `inline` specifier was defined specifically so identical instantiations of the *same* function template would be merged, which would seem (at least somewhat) extraneous if they could be merged without it. If instantiations of the same template can't be merged without `inline`, it seems unlikely that instantiations of different templates could. That said, if you really care, you probably just want to use `/OPT:NOICF` with MS' linker to prevent it. – Jerry Coffin Jun 07 '23 at 17:49
  • 2
    @Fedor that's wild! That doesn't feel right to me, although with the differences between `constexpr` and runtime, I guess maybe it's compliant? It acts the same with function-templates: https://gcc.godbolt.org/z/17MqMzWzG – Ben Jun 07 '23 at 18:26
2

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.

Fedor
  • 17,146
  • 13
  • 40
  • 131
  • So it sounds like `/OPT:ICF` breaks compatibility in the specific case of function pointers, even if (if I read the replies correctly), the MSVC Release behavior of my initial question (and possibly function templates cast to function pointers) may be compliant. The discrepancy between `constexpr` and runtime equality is the biggest WTF for me. Can that really be compliant? – Ben Jun 08 '23 at 16:26
  • @Ben, I think it is not compliant, but I cannot find the quotation from the standard. – Fedor Jun 09 '23 at 15:23