Date: Sat, 26 Apr 2025 14:53:51 +0100
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]> 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
```
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]> 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
Received on 2025-04-26 13:53:57