Date: Mon, 17 Aug 2026 11:19:10 -0400
On Mon, Aug 17, 2026 at 10:56 AM Kim J. Smith via SG14 <
sg14_at_[hidden]> wrote:
> > `F` satisfies the current `is_empty_trivial` test, but constructing an
> owning wrapper from `f` would normally construct a target object and
> therefore invoke the copy constructor. Eliminating the object also
> eliminates that observable effect.
> >
> > More generally, an empty object does not imply that its identity or
> lifetime is unobservable; `operator()` may also use `this`. So I suspect
> that trying to infer semantic statelessness from representation/type traits
> is difficult to make generally safe.
>
> This is indeed an area where I didn't think things through thoroughly. I
> used to think `is_empty_trivial` does not indicate stateless only in
> non-owning semantics (`IsView` == true) and ignored the side effects in the
> lifetime management in owning semantics.
>
> There are basically two categories of side effects:
> 1. side effects in lifetime management;
> 2. use of `this` in `operator()`.
>
> For the first category, adding `std::is_trivially_move_constructible` and
> `std::is_trivially_copy_constructible` would be sufficient to eliminate the
> issue. However, there seems to be no reliable way to eliminate the second
> category.
>
You could "eliminate" it by relaxing the guarantees you document for your
wrapper. Instead of documenting that you always call the stored callable,
you could document that you call "the stored callable, or a copy of it,"
and furthermore document that you do *not* make copies *unless* the stored
callable is trivially copyable (i.e. you guarantee never to make an
expensive copy).
There is then no observable difference between
EmptyTrivial t_; // trivially default-constructible, trivially
copyable, and empty
void operator()() const { auto copy = t_; copy(); } // what you
document
and
void operator()() const { EmptyTrivial copy; copy(); } // what you
actually do
In both cases, `©` is documented to be not-necessarily-the-same as
`&t_`.
Vice versa, a type with a static operator() can't use `this` inside its
`operator()`, but it could still have a non-trivial default constructor
and/or copy and/or destructor.
I wouldn't try to detect static operator(); that seems like it could lead
to SFINAE-unfriendly errors when the user wasn't expecting it.
template<class T, class... Args>
concept StaticCallable = requires {
T::operator()(std::declval<Args>()...); };
You might also find pathological types such that `T::operator()(args...)`
is well-formed but `t(args...)` calls a different overload.
(Yes, you can also find pathological types that are trivially copyable but
`auto copy = t_;` calls a different overload! But that's a long-standing
and "well-known" pathology: If someone gives you a type like that, I feel
perfectly justified in saying "don't do that then." Playing games with
static `operator()` is obviously newer and less well-studied.)
–Arthur
sg14_at_[hidden]> wrote:
> > `F` satisfies the current `is_empty_trivial` test, but constructing an
> owning wrapper from `f` would normally construct a target object and
> therefore invoke the copy constructor. Eliminating the object also
> eliminates that observable effect.
> >
> > More generally, an empty object does not imply that its identity or
> lifetime is unobservable; `operator()` may also use `this`. So I suspect
> that trying to infer semantic statelessness from representation/type traits
> is difficult to make generally safe.
>
> This is indeed an area where I didn't think things through thoroughly. I
> used to think `is_empty_trivial` does not indicate stateless only in
> non-owning semantics (`IsView` == true) and ignored the side effects in the
> lifetime management in owning semantics.
>
> There are basically two categories of side effects:
> 1. side effects in lifetime management;
> 2. use of `this` in `operator()`.
>
> For the first category, adding `std::is_trivially_move_constructible` and
> `std::is_trivially_copy_constructible` would be sufficient to eliminate the
> issue. However, there seems to be no reliable way to eliminate the second
> category.
>
You could "eliminate" it by relaxing the guarantees you document for your
wrapper. Instead of documenting that you always call the stored callable,
you could document that you call "the stored callable, or a copy of it,"
and furthermore document that you do *not* make copies *unless* the stored
callable is trivially copyable (i.e. you guarantee never to make an
expensive copy).
There is then no observable difference between
EmptyTrivial t_; // trivially default-constructible, trivially
copyable, and empty
void operator()() const { auto copy = t_; copy(); } // what you
document
and
void operator()() const { EmptyTrivial copy; copy(); } // what you
actually do
In both cases, `©` is documented to be not-necessarily-the-same as
`&t_`.
Vice versa, a type with a static operator() can't use `this` inside its
`operator()`, but it could still have a non-trivial default constructor
and/or copy and/or destructor.
I wouldn't try to detect static operator(); that seems like it could lead
to SFINAE-unfriendly errors when the user wasn't expecting it.
template<class T, class... Args>
concept StaticCallable = requires {
T::operator()(std::declval<Args>()...); };
You might also find pathological types such that `T::operator()(args...)`
is well-formed but `t(args...)` calls a different overload.
(Yes, you can also find pathological types that are trivially copyable but
`auto copy = t_;` calls a different overload! But that's a long-standing
and "well-known" pathology: If someone gives you a type like that, I feel
perfectly justified in saying "don't do that then." Playing games with
static `operator()` is obviously newer and less well-studied.)
–Arthur
Received on 2026-08-17 15:19:30
