Date: Sat, 26 Apr 2025 18:22:42 +0200
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_[hidden] <mailto:std-
> discussion_at_[hidden]>> 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] <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
> 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_[hidden] <mailto:std-
> discussion_at_[hidden]>> 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] <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
Received on 2025-04-26 16:22:49