C++ Logo

std-discussion

Advanced search

Re: Guarantees over addresses from function pointers created from lambda

From: Tiago Freire <tmiguelf_at_[hidden]>
Date: Sat, 26 Apr 2025 20:06:45 +0000
I personally see no reason why "no such guarantees" shouldn't be part of the standard.
I see no reason as to why comparing function addresses should yield predictable results, and saving spaces seems to me like an overwhelming compelling case as why 2 functions could have the exact same address.

-----Original Message-----
From: Std-Discussion <std-discussion-bounces_at_lists.isocpp.org> On Behalf Of Jens Maurer via Std-Discussion
Sent: Saturday, April 26, 2025 9:53 PM
To: std-discussion_at_[hidden]; mauro russo <ing.russomauro_at_gmail.com>
Cc: Jens Maurer <jens.maurer_at_gmx.net>
Subject: Re: [std-discussion] Guarantees over addresses from function pointers created from lambda



On 26/04/2025 19.43, Jennifier Burnett via Std-Discussion wrote:
> Quoting [expr.eq]p2:
>
>> Comparing pointers
> is defined as follows: Two pointers compare equal if they are both
> null, both point to the same function, or both represent the same
> address (3.9.2), otherwise they compare unequal
>
> Specifically we're interested in "Two pointers compare equal if ... both point to the same function".
>
> Given
>
> ```
> void foo(){}
> void bar(){}
> ```
>
> The only way in which `&foo == &bar` could be true is if you say that both foo and bar are the same function. Since they have different names, that to me would would exclude them from being the same function.

Right, but as was already said on this thread, optimizers violate that provision quite often.

Could you demonstrate why &foo and &bar not being the same wold matter to start with? They have the same behavior.

Jens


> On 26 April 2025 17:22:42 BST, Federico Kircheis via Std-Discussion <std-discussion_at_lists.isocpp.org> wrote:
>> On 26/04/2025 4:05 pm, mauro russo wrote:
>>> from the original questions:
>>>
>>> > * if the same function can have different addresses, the class
>>> is > inefficient (destructing and creating objects instead of
>>> assigning them)
>>>
>>> Jennifer provided already an answer: different template parameters,
>>> means different instantiations, therefore distinct closure types (about lambda, the same signature leads anyway to distinct types).
>>
>> Yes, but the function pointers have the same type.
>>
>> The point that I was trying to make, is that in case of trivial type,
>> the following snippet
>>
>> .copy = [](void* dest, const void* source) { ::new (dest)
>> T(*std::launder(reinterpret_cast<const T*>(source)));},
>>
>> is equivalent to
>>
>> .copy = [](void* dest, const void* source) { std::memcpy(dest,
>> source, sizeof(T);}
>>
>> Since sizeof(T) is just a constant number, the body of the lambda does not depend on T anymore.
>>
>>> > * if different function can have the same address, the class if
>>> > broken, (the lifetime of the previous object did not end
>>> correctly)
>>>
>>> This should be something never happening. It cannot make sense to
>>> have different functions at the same address.
>>
>> IMHO it makes sense that
>>
>> void foo(){}
>> void bar(){}
>>
>> can have the same address, similarly to how identical string literals can have the same address; I was not sure if such optimization was allowed.
>>
>>> Anyway, in your draft code, please consider that you are assigning, for example, to f.copy a local lambda whose lifetime ends when your constructor ends executing.
>>
>>
>> Mhm, f.copy is assigned to a function pointer created through a lambda, not a pointer to the lambda.
>> It was my understanding that those functions have global lifetimes, like static functions.
>>
>>> my 2 c.
>>> Mauro.
>>>
>>> Il giorno sab 26 apr 2025 alle ore 15:54 Jennifier Burnett via Std- Discussion <std-discussion_at_lists.isocpp.org <mailto:std- discussion_at_lists.isocpp.org>> ha scritto:
>>>
>>> To simplify this question a bit, given how the standard defines
>>> lambdas you are asking if given the following:
>>>
>>> ```
>>> struct A
>>> {
>>> static void foo() {}
>>> };
>>> struct B
>>> {
>>> static void foo() {}
>>> }
>>> ```
>>>
>>> If it is possible that `&A::foo == &B::foo` can return `true`.
>>>
>>> Relevant transformations are in [expr.prim.lambda]p6, plus using the
>>> fact that instantiations of a templated type with different template
>>> parameters are distinct types.
>>>
>>>
>>>
>>>
>>>
>>> On 26 April 2025 13:41:51 BST, Federico Kircheis via Std-Discussion
>>> <std-discussion_at_[hidden]rg <mailto:std-
>>> discussion_at_[hidden]>> wrote:
>>> >Hello,
>>> >
>>> >I'm trying to understand if the standard does guarantee anything
>>> about the uniqueness of function pointers created by lambda (thus
>>> lambdas with empty captures).
>>> >
>>> >In particular, given a template factory that generates functions
>>> through a lambda (the template parameter is used inside the lambda,
>>> no capture takes place):
>>> >
>>> >1) are the function pointers the same for the same template parameter?
>>> >2) if the template parameter does not affect the generated
>>> assembly, can two functions point to the same addresses?
>>> >
>>> >
>>> >
>>> >
>>> >
>>> >
>>> >
>>> >
>>> >
>>> >
>>> >In case anyone is wondering why I'm interested in those
>>> properties, or my question is unclear and you are wondering what I
>>> really want to know...
>>> >
>>> >
>>> >Consider the following class "nontrivial_storage":
>>> >
>>> >
>>> >----
>>> >template <int N>
>>> >class nontrivial_storage {
>>> > alignas(N) unsigned char buffer[N] = {};
>>> >
>>> >    struct {
>>> >        using copy_fun_t = void(void*, const void*);
>>> > using assign_copy_fun_t = void(void*, const void*);
>>> > using move_fun_t = void(void*, void*) noexcept;
>>> > using assign_move_fun_t = void(void*, void*) noexcept;
>>> > using dest_fun_t = void(void*) noexcept;
>>> > copy_fun_t* copy;
>>> > assign_copy_fun_t* assign_copy;
>>> > move_fun_t* move;
>>> > assign_move_fun_t* assign_move;
>>> > dest_fun_t* destroy;
>>> > } f;
>>> >
>>> > public:
>>> > template <typename T>
>>> > explicit nontrivial_storage(const T& t) noexcept :
>>> >          f{
>>> >              .copy = [](void* dest, const void* source) { ::new
>>> (dest) T(*std::launder(reinterpret_cast<const T*>(source)));},
>>> > .assign_copy = [](void* dest, const void* source)
>>> {*std::launder(reinterpret_cast<T*>(dest)) =
>>> *std::launder(reinterpret_cast<const T*>(source));},
>>> > .move = [](void* dest, void* source) noexcept
>>> { ::new (dest)
>>> T(std::move(*std::launder(reinterpret_cast<T*>(source))));},
>>> >              .assign_move = [](void* dest, void* source) noexcept
>>> {*std::launder(reinterpret_cast<T*>(dest)) =
>>> std::move(*std::launder(reinterpret_cast<const T*>(source)));},
>>> > .destroy = [](void* dest) noexcept
>>> { std::launder(reinterpret_cast<T*>(dest))->~T(); },
>>> > } {
>>> > f.copy(buffer, &t);
>>> > static_assert(std::is_nothrow_move_constructible_v<T>,
>>> "otherwise move op be noexcept(false)");
>>> > }
>>> > nontrivial_storage(const nontrivial_storage& other) : f(other.f) {
>>> > f.copy(this->buffer, other->buffer);
>>> > }
>>> > nontrivial_storage& operator=(const nontrivial_storage& other) {
>>> > if (&other != this) {
>>> > if(other.f.assign_copy != this->f.assign_copy){
>>> > f.destroy(this->buffer);
>>> > other.f.copy(this->buffer, other->buffer);
>>> > }else{
>>> > this->f.assign_copy(this->buffer, other.buffer);
>>> > }
>>> > this->f = other.f;
>>> > }
>>> > return *this;
>>> > }
>>> >
>>> > // move operations and destruction
>>> > // ...
>>> >
>>> >};
>>> >----
>>> >
>>> >It uses "if(other.f.assign_copy != this->f.assign_copy)" to
>>> determine if the contained type-erased type is the same without
>>> storing anywhere information about the type (for example with
>>> type_info).
>>> >But if the "same function" can have different addresses or (worse)
>>> different functions can have the same address, then it this class is
>>> problematic.
>>> >
>>> >
>>> >(My) experience shows that
>>> >
>>> >[](void* dest, const void* source)
>>> {*std::launder(reinterpret_cast<T*>(dest)) =
>>> *std::launder(reinterpret_cast<const T*>(source));}
>>> >
>>> >generates the same function address for the same template, and
>>> different addresses for different templates.
>>> >
>>> >A valid implementation of the lambda could "just" memcpy sizeof(T)
>>> bytes (for example if T is an integral type), thus for different
>>> templates, the function body would be 100% equivalent (from a
>>> compiler POV).
>>> >
>>> >Thus:
>>> >
>>> > * if the same function can have different addresses, the class is
>>> inefficient (destructing and creating objects instead of assigning them)
>>> > * if different function can have the same address, the class if
>>> broken, (the lifetime of the previous object did not end correctly)
>>> >
>>> >
>>> >Best
>>> >
>>> >Federico
>>

--
Std-Discussion mailing list
Std-Discussion_at_[hidden]
https://lists.isocpp.org/mailman/listinfo.cgi/std-discussion

Received on 2025-04-26 20:06:48