Date: Thu, 23 Oct 2025 12:26:20 +0000
I reply in series below to Simon and Arthur.
On Thu, Oct 23, 2025 at 4:56 AM Simon Schröder wrote:
>
> Currently, C++ does not have a concept of suspended lifetimes.
> This means this can only be a proposal if we heavily change the
> wording of the C++ standard to include this new concept. If no
> one is willing to do this, we can stop the discussion right now.
I'm not so sure we would have to bring "suspended lifetimes" into the
Standard, but perhaps just provide a way of tagging a class to
indicate that it should never be put into some sort of suspended
stasis. The tag might be "never_retain", or "never_stasis".
> On Oct 22, 2025, at 8:14 PM, Arthur O'Dwyer wrote:
>
> <snip> . . . which relies on the idea that you can memcpy the
> representation out of an object, then memcpy it back (to the
> exact same address), and continue to use the object. This is true
> on all platforms I'm aware of, but technically not guaranteed by the Standard.
Some undefined behaviour works on 90% of compilers, some works on 95%
of compilers, some works on 99% of compilers, and some works on every
compiler. A good example of 100%-above-board undefined behaviour is
returning a locked mutex from a function by value:
mutex Func(void)
{
struct Monkey : mutex {
Monkey(void)
{
this->lock();
}
};
constexpr auto f = +[](){ return Monkey(); };
auto f2 = (mutex (*)(void))f;
return f2();
}
You won't find a compiler on the face of the Earth that doesn't
compile the above as intended.
But anyway moving on to your implementation of 'replace':
> template<class T, class... Args>
> void replace(T *where, Args&&... args) {
> static_assert(std::is_nothrow_destructible_v<T>);
> if (std::is_nothrow_constructible_v<T, Args...>) {
>
> std::destroy_at(where);
> std::construct_at(where, std::forward<Args>(args)...);
>
> } else if (std::is_nothrow_relocatable_v<T, Args...>) {
>
> alignas(T) char elsewhere[sizeof(T)];
> std::relocate_at(where, (T*)elsewhere);
> try {
> std::construct_at(where, std::forward<Args>(args)...);
> } catch (...) {
> std::relocate_at((T*)elsewhere, where);
> throw;
> }
> std::destroy_at((T*)elsewhere);
>
> } else {
> // We can't really do it in a nice way, but we can use FVG's approach:
>
> char elsewhere[sizeof(T)];
> std::memcpy(elsewhere, where, sizeof(T));
> try {
> std::construct_at(where, std::forward<Args>(args)...);
> } catch (...) {
> std::memcpy(where, elsewhere, sizeof(T));
> throw;
> }
> char yetagain[sizeof(T)];
> std::memcpy(yetagain, where, sizeof(T));
> std::memcpy(where, elsewhere, sizeof(T));
> std::destroy_at(where);
> std::memcpy(where, yetagain, sizeof(T));
>
> }
> }
This is a really good start, let's beef it out a bit, but before we do
so -- and I've emailed you about this before with regard to your
'Auto' macro -- I want to talk about destructors throwing exceptions.
When I build a program in Release Mode, I want it to hobble on
valiantly when half the program is failing; I don't want it to crash
unless absolutely necessary, and so I disregard any exceptions thrown
from destructors. But in Debug Mode, I pay heed to those exceptions
because I want to fix stuff in my code.
In your code above, you have:
static_assert(std::is_nothrow_destructible_v<T>);
I think this is too restrictive so I've taken it out. Instead, in
Release Mode I catch and ignore, but in Debug Mode I catch and end the
program. Here's what I've got at the moment:
https://godbolt.org/z/nbeKMKe1o (Note that I use Arthur's P1144 compiler)
And here it is copy-pasted:
#include <cassert> // assert
#include <cstddef> // byte
#include <cstring> // memcpy
#include <algorithm> // swap_ranges
#include <memory> // construct_at, destroy_at
#include <type_traits> // type traits
#include <utility> // forward, move
#ifdef NDEBUG
# define CRASH_IN_DEBUG_MODE() /* nothing */
#else
# define CRASH_IN_DEBUG_MODE() assert(nullptr == "Destructor
threw an exception!")
#endif
#if defined(HAS_STD_RELOCATE_AT) && defined(HAS_STD_IS_NOTHROW_RELOCATABLE)
# define CAN_USE_STD_RELOCATE 1
#else
# define CAN_USE_STD_RELOCATE 0
#endif
template<typename T, typename... Args>
requires (!std::is_const_v<T> && !std::is_volatile_v<T>)
void replace(T *const where, Args&&... args)
noexcept(std::is_nothrow_constructible_v<T, Args...>)
{
using std::construct_at;
using std::destroy_at;
if constexpr ( std::is_nothrow_constructible_v<T, Args...> )
{
try { destroy_at(where); } catch (...) { CRASH_IN_DEBUG_MODE(); }
construct_at(where, std::forward<Args>(args)...);
}
#if CAN_USE_STD_RELOCATE
else if constexpr ( true )
{
alignas(T) std::byte elsewhere[sizeof(T)];
T *const elsewhereT = static_cast<T*>(static_cast<void*>(elsewhere));
// Relocation is supported (non-standard extension)
if constexpr ( std::is_nothrow_relocatable_v<T> )
{
std::relocate_at(where, elsewhereT);
try { construct_at(where, std::forward<Args>(args)...); }
catch (...)
{
std::relocate_at(elsewhereT, where);
throw;
}
try { destroy_at(elsewhereT); } catch (...) {
CRASH_IN_DEBUG_MODE(); }
}
else if constexpr ( std::is_nothrow_move_constructible_v<T> )
{
// Relocation exists but isn't noexcept — fallback to move
construct_at(elsewhereT, std::move(*where));
try { destroy_at(where); } catch (...) { CRASH_IN_DEBUG_MODE(); }
try { construct_at(where, std::forward<Args>(args)...); }
catch (...)
{
construct_at(where, std::move(*elsewhereT));
try { destroy_at(elsewhereT); } catch (...) {
CRASH_IN_DEBUG_MODE(); }
throw;
}
try { destroy_at(elsewhereT); } catch (...) {
CRASH_IN_DEBUG_MODE(); }
}
else
{
static_assert( false == requires { typename
T::tag_never_retain; } );
using std::memcpy;
std::byte elsewhere[sizeof(T)];
memcpy(elsewhere, where, sizeof *elsewhere);
try { construct_at(where, std::forward<Args>(args)...); }
catch (...)
{
memcpy(where, elsewhere, sizeof *where);
throw;
}
std::swap_ranges(
elsewhere,
elsewhere + sizeof elsewhere,
static_cast<std::byte*>(static_cast<void*>(where))
);
try { destroy_at(where); } catch (...) { CRASH_IN_DEBUG_MODE(); }
memcpy(where, elsewhere, sizeof *where);
}
}
#endif // if CAN_USE_STD_RELOCATE
else if constexpr (std::is_nothrow_move_constructible_v<T>)
{
// No relocation support; fallback to move construction
alignas(T) std::byte elsewhere[sizeof(T)];
T *const elsewhereT = static_cast<T*>(static_cast<void*>(elsewhere));
construct_at(elsewhereT, std::move(*where));
try { destroy_at(where); } catch (...) { CRASH_IN_DEBUG_MODE(); }
try { construct_at(where, std::forward<Args>(args)...); }
catch (...)
{
construct_at(where, std::move(*elsewhereT));
try { destroy_at(elsewhereT); } catch (...) {
CRASH_IN_DEBUG_MODE(); }
throw;
}
try { destroy_at(elsewhereT); } catch (...) { CRASH_IN_DEBUG_MODE(); }
}
else
{
static_assert( false == requires { typename T::tag_never_retain; } );
using std::memcpy;
std::byte elsewhere[sizeof(T)];
memcpy(elsewhere, where, sizeof *elsewhere);
try { construct_at(where, std::forward<Args>(args)...); }
catch (...)
{
memcpy(where, elsewhere, sizeof *where);
throw;
}
std::swap_ranges(
elsewhere,
elsewhere + sizeof elsewhere,
static_cast<std::byte*>(static_cast<void*>(where))
);
try { destroy_at(where); } catch (...) { CRASH_IN_DEBUG_MODE(); }
memcpy(where, elsewhere, sizeof *where);
}
}
#include <string>
int main(void)
{
struct Monkey {
Monkey(void) noexcept(false) = default;
Monkey(Monkey&) = delete;
Monkey(Monkey&&) = delete;
typedef int tag_never_retain2; // try removing the '2' here
};
Monkey var;
replace(&var);
std::string str;
replace(&str);
}
On Thu, Oct 23, 2025 at 4:56 AM Simon Schröder wrote:
>
> Currently, C++ does not have a concept of suspended lifetimes.
> This means this can only be a proposal if we heavily change the
> wording of the C++ standard to include this new concept. If no
> one is willing to do this, we can stop the discussion right now.
I'm not so sure we would have to bring "suspended lifetimes" into the
Standard, but perhaps just provide a way of tagging a class to
indicate that it should never be put into some sort of suspended
stasis. The tag might be "never_retain", or "never_stasis".
> On Oct 22, 2025, at 8:14 PM, Arthur O'Dwyer wrote:
>
> <snip> . . . which relies on the idea that you can memcpy the
> representation out of an object, then memcpy it back (to the
> exact same address), and continue to use the object. This is true
> on all platforms I'm aware of, but technically not guaranteed by the Standard.
Some undefined behaviour works on 90% of compilers, some works on 95%
of compilers, some works on 99% of compilers, and some works on every
compiler. A good example of 100%-above-board undefined behaviour is
returning a locked mutex from a function by value:
mutex Func(void)
{
struct Monkey : mutex {
Monkey(void)
{
this->lock();
}
};
constexpr auto f = +[](){ return Monkey(); };
auto f2 = (mutex (*)(void))f;
return f2();
}
You won't find a compiler on the face of the Earth that doesn't
compile the above as intended.
But anyway moving on to your implementation of 'replace':
> template<class T, class... Args>
> void replace(T *where, Args&&... args) {
> static_assert(std::is_nothrow_destructible_v<T>);
> if (std::is_nothrow_constructible_v<T, Args...>) {
>
> std::destroy_at(where);
> std::construct_at(where, std::forward<Args>(args)...);
>
> } else if (std::is_nothrow_relocatable_v<T, Args...>) {
>
> alignas(T) char elsewhere[sizeof(T)];
> std::relocate_at(where, (T*)elsewhere);
> try {
> std::construct_at(where, std::forward<Args>(args)...);
> } catch (...) {
> std::relocate_at((T*)elsewhere, where);
> throw;
> }
> std::destroy_at((T*)elsewhere);
>
> } else {
> // We can't really do it in a nice way, but we can use FVG's approach:
>
> char elsewhere[sizeof(T)];
> std::memcpy(elsewhere, where, sizeof(T));
> try {
> std::construct_at(where, std::forward<Args>(args)...);
> } catch (...) {
> std::memcpy(where, elsewhere, sizeof(T));
> throw;
> }
> char yetagain[sizeof(T)];
> std::memcpy(yetagain, where, sizeof(T));
> std::memcpy(where, elsewhere, sizeof(T));
> std::destroy_at(where);
> std::memcpy(where, yetagain, sizeof(T));
>
> }
> }
This is a really good start, let's beef it out a bit, but before we do
so -- and I've emailed you about this before with regard to your
'Auto' macro -- I want to talk about destructors throwing exceptions.
When I build a program in Release Mode, I want it to hobble on
valiantly when half the program is failing; I don't want it to crash
unless absolutely necessary, and so I disregard any exceptions thrown
from destructors. But in Debug Mode, I pay heed to those exceptions
because I want to fix stuff in my code.
In your code above, you have:
static_assert(std::is_nothrow_destructible_v<T>);
I think this is too restrictive so I've taken it out. Instead, in
Release Mode I catch and ignore, but in Debug Mode I catch and end the
program. Here's what I've got at the moment:
https://godbolt.org/z/nbeKMKe1o (Note that I use Arthur's P1144 compiler)
And here it is copy-pasted:
#include <cassert> // assert
#include <cstddef> // byte
#include <cstring> // memcpy
#include <algorithm> // swap_ranges
#include <memory> // construct_at, destroy_at
#include <type_traits> // type traits
#include <utility> // forward, move
#ifdef NDEBUG
# define CRASH_IN_DEBUG_MODE() /* nothing */
#else
# define CRASH_IN_DEBUG_MODE() assert(nullptr == "Destructor
threw an exception!")
#endif
#if defined(HAS_STD_RELOCATE_AT) && defined(HAS_STD_IS_NOTHROW_RELOCATABLE)
# define CAN_USE_STD_RELOCATE 1
#else
# define CAN_USE_STD_RELOCATE 0
#endif
template<typename T, typename... Args>
requires (!std::is_const_v<T> && !std::is_volatile_v<T>)
void replace(T *const where, Args&&... args)
noexcept(std::is_nothrow_constructible_v<T, Args...>)
{
using std::construct_at;
using std::destroy_at;
if constexpr ( std::is_nothrow_constructible_v<T, Args...> )
{
try { destroy_at(where); } catch (...) { CRASH_IN_DEBUG_MODE(); }
construct_at(where, std::forward<Args>(args)...);
}
#if CAN_USE_STD_RELOCATE
else if constexpr ( true )
{
alignas(T) std::byte elsewhere[sizeof(T)];
T *const elsewhereT = static_cast<T*>(static_cast<void*>(elsewhere));
// Relocation is supported (non-standard extension)
if constexpr ( std::is_nothrow_relocatable_v<T> )
{
std::relocate_at(where, elsewhereT);
try { construct_at(where, std::forward<Args>(args)...); }
catch (...)
{
std::relocate_at(elsewhereT, where);
throw;
}
try { destroy_at(elsewhereT); } catch (...) {
CRASH_IN_DEBUG_MODE(); }
}
else if constexpr ( std::is_nothrow_move_constructible_v<T> )
{
// Relocation exists but isn't noexcept — fallback to move
construct_at(elsewhereT, std::move(*where));
try { destroy_at(where); } catch (...) { CRASH_IN_DEBUG_MODE(); }
try { construct_at(where, std::forward<Args>(args)...); }
catch (...)
{
construct_at(where, std::move(*elsewhereT));
try { destroy_at(elsewhereT); } catch (...) {
CRASH_IN_DEBUG_MODE(); }
throw;
}
try { destroy_at(elsewhereT); } catch (...) {
CRASH_IN_DEBUG_MODE(); }
}
else
{
static_assert( false == requires { typename
T::tag_never_retain; } );
using std::memcpy;
std::byte elsewhere[sizeof(T)];
memcpy(elsewhere, where, sizeof *elsewhere);
try { construct_at(where, std::forward<Args>(args)...); }
catch (...)
{
memcpy(where, elsewhere, sizeof *where);
throw;
}
std::swap_ranges(
elsewhere,
elsewhere + sizeof elsewhere,
static_cast<std::byte*>(static_cast<void*>(where))
);
try { destroy_at(where); } catch (...) { CRASH_IN_DEBUG_MODE(); }
memcpy(where, elsewhere, sizeof *where);
}
}
#endif // if CAN_USE_STD_RELOCATE
else if constexpr (std::is_nothrow_move_constructible_v<T>)
{
// No relocation support; fallback to move construction
alignas(T) std::byte elsewhere[sizeof(T)];
T *const elsewhereT = static_cast<T*>(static_cast<void*>(elsewhere));
construct_at(elsewhereT, std::move(*where));
try { destroy_at(where); } catch (...) { CRASH_IN_DEBUG_MODE(); }
try { construct_at(where, std::forward<Args>(args)...); }
catch (...)
{
construct_at(where, std::move(*elsewhereT));
try { destroy_at(elsewhereT); } catch (...) {
CRASH_IN_DEBUG_MODE(); }
throw;
}
try { destroy_at(elsewhereT); } catch (...) { CRASH_IN_DEBUG_MODE(); }
}
else
{
static_assert( false == requires { typename T::tag_never_retain; } );
using std::memcpy;
std::byte elsewhere[sizeof(T)];
memcpy(elsewhere, where, sizeof *elsewhere);
try { construct_at(where, std::forward<Args>(args)...); }
catch (...)
{
memcpy(where, elsewhere, sizeof *where);
throw;
}
std::swap_ranges(
elsewhere,
elsewhere + sizeof elsewhere,
static_cast<std::byte*>(static_cast<void*>(where))
);
try { destroy_at(where); } catch (...) { CRASH_IN_DEBUG_MODE(); }
memcpy(where, elsewhere, sizeof *where);
}
}
#include <string>
int main(void)
{
struct Monkey {
Monkey(void) noexcept(false) = default;
Monkey(Monkey&) = delete;
Monkey(Monkey&&) = delete;
typedef int tag_never_retain2; // try removing the '2' here
};
Monkey var;
replace(&var);
std::string str;
replace(&str);
}
Received on 2025-10-23 12:26:36
