Date: Sat, 15 Jul 2023 14:03:04 +0100
Let's say I have a function as follows that might throw an exception:
class MyClass { ........ };
MyClass Func(int, double, char*) noexcept(false);
I decide to wrap this function in a function that never throws:
thread_local std::exception_ptr e;
MyClass FuncNoThrow(int a, double b, char *c) noexcept
{
try { return Func(a,b,c); }
catch(...) { e = std::current_exception(); }
return {};
}
That return statement at the end is really just to suppress a compiler
warning and also to avoid what strictly-speaking is 'undefined
behaviour' -- you're not allowed to fall off the end of a
non-void-returning function.
In the System V x86_64 calling convention, a return statement can do
three different things depending on how big the return value is:
(1) If the return value <= 8 bytes, it just sets the RAX register
(2) If 8 bytes < return value <= 16 bytes, it sets both RAX and RDX
(3) if return value > 16 bytes, it stores the return value in the
memory pointed to by RDI
So if you don't have a return statement in a non-void-returning
function, then there won't be any real drama so long as you don't try
to access the return value in the caller function (because you're not
accessing RAX, RDX, or memory pointed to by RDI). Nonetheless, the C++
Standard says that it's undefined behaviour irrespective of whether
you try to do anything with the return value. (I can see however see
how there would be a problem if MyClass had a nontrivial destructor).
FuncNoThrow could be utilised as follows:
void Monkey(void)
{
e = nullptr;
auto retval = FuncNoThrow(5, 6.2, nullptr);
if ( nullptr != e ) std::rethrow_exception(e);
cout << retval << endl;
}
This will all work fine so long as 'MyClass' has a default
constructor. If it doesn't, then the "return {};" won't compile. If
there's no default constructor, we could try something like:
MyClass FuncNoThrow(int a, double b, char *c) noexcept
{
try { return FunctionName(a,b,c); }
catch(...) { e = std::current_exception(); }
alignas(MyClass) static char dummy[sizeof(MyClass)] = {};
return *static_cast<MyClass*>(static_cast<void*>(dummy));
}
This would work fine unless the 'move constructor' for MyClass gets
invoked and tries to copy data to a nullptr. What I really want here
is the mandatory elision of copy/move operations provided by "return
value optimisation", but unfortunately it's not provided in this case.
Somehow I need to get a PRvalue in order for elision to be mandatory.
So maybe something along the lines of:
void dummy_prvalue_detail(void) {}
template<typename T> requires std::is_trivially_destructible_v<
std::remove_cvref_t<T> >
std::remove_cvref_t<T> dummy_prvalue(void) noexcept
{
typedef std::remove_cvref_t<T> TT;
TT (*const funcptr)(void) = reinterpret_cast<TT(*)(void)>(
dummy_prvalue_detail );
return funcptr(); // guaranteed elision of move/copy operations here
}
What I really need here is something along the lines of 'std::declval' but:
(1) It can be evaluated
(2) It gives you a PRvalue (ensuring elision for return value optimisation)
(3) It is safe and its behaviour is well-defined so long as you don't
try to access the non-existent object
(4) It will reject types that have a non-trivial destructor
An alternative to this would be to allow non-void-returning functions
to just fall off the end, so long as the return value is not used in
the caller function, meaning we'd just have:
MyClass FuncNoThrow(int a, double b, char *c) noexcept
{
try { return Func(a,b,c); }
catch(...) { e = std::current_exception(); }
// falling off the end here is fine so long
// as MyClass has a trivial destructor, and
// so long as you don't use the return value
}
So maybe C++26 could either:
(a) Allow the falling off the end of non-void-returning functions so
long as the return type is trivially destructible.
or:
(b) Have in the standard library a "dummy_prvalue" template function
such as what I've written, with well-defined behaviour so long as you
don't try to access the return value.
class MyClass { ........ };
MyClass Func(int, double, char*) noexcept(false);
I decide to wrap this function in a function that never throws:
thread_local std::exception_ptr e;
MyClass FuncNoThrow(int a, double b, char *c) noexcept
{
try { return Func(a,b,c); }
catch(...) { e = std::current_exception(); }
return {};
}
That return statement at the end is really just to suppress a compiler
warning and also to avoid what strictly-speaking is 'undefined
behaviour' -- you're not allowed to fall off the end of a
non-void-returning function.
In the System V x86_64 calling convention, a return statement can do
three different things depending on how big the return value is:
(1) If the return value <= 8 bytes, it just sets the RAX register
(2) If 8 bytes < return value <= 16 bytes, it sets both RAX and RDX
(3) if return value > 16 bytes, it stores the return value in the
memory pointed to by RDI
So if you don't have a return statement in a non-void-returning
function, then there won't be any real drama so long as you don't try
to access the return value in the caller function (because you're not
accessing RAX, RDX, or memory pointed to by RDI). Nonetheless, the C++
Standard says that it's undefined behaviour irrespective of whether
you try to do anything with the return value. (I can see however see
how there would be a problem if MyClass had a nontrivial destructor).
FuncNoThrow could be utilised as follows:
void Monkey(void)
{
e = nullptr;
auto retval = FuncNoThrow(5, 6.2, nullptr);
if ( nullptr != e ) std::rethrow_exception(e);
cout << retval << endl;
}
This will all work fine so long as 'MyClass' has a default
constructor. If it doesn't, then the "return {};" won't compile. If
there's no default constructor, we could try something like:
MyClass FuncNoThrow(int a, double b, char *c) noexcept
{
try { return FunctionName(a,b,c); }
catch(...) { e = std::current_exception(); }
alignas(MyClass) static char dummy[sizeof(MyClass)] = {};
return *static_cast<MyClass*>(static_cast<void*>(dummy));
}
This would work fine unless the 'move constructor' for MyClass gets
invoked and tries to copy data to a nullptr. What I really want here
is the mandatory elision of copy/move operations provided by "return
value optimisation", but unfortunately it's not provided in this case.
Somehow I need to get a PRvalue in order for elision to be mandatory.
So maybe something along the lines of:
void dummy_prvalue_detail(void) {}
template<typename T> requires std::is_trivially_destructible_v<
std::remove_cvref_t<T> >
std::remove_cvref_t<T> dummy_prvalue(void) noexcept
{
typedef std::remove_cvref_t<T> TT;
TT (*const funcptr)(void) = reinterpret_cast<TT(*)(void)>(
dummy_prvalue_detail );
return funcptr(); // guaranteed elision of move/copy operations here
}
What I really need here is something along the lines of 'std::declval' but:
(1) It can be evaluated
(2) It gives you a PRvalue (ensuring elision for return value optimisation)
(3) It is safe and its behaviour is well-defined so long as you don't
try to access the non-existent object
(4) It will reject types that have a non-trivial destructor
An alternative to this would be to allow non-void-returning functions
to just fall off the end, so long as the return value is not used in
the caller function, meaning we'd just have:
MyClass FuncNoThrow(int a, double b, char *c) noexcept
{
try { return Func(a,b,c); }
catch(...) { e = std::current_exception(); }
// falling off the end here is fine so long
// as MyClass has a trivial destructor, and
// so long as you don't use the return value
}
So maybe C++26 could either:
(a) Allow the falling off the end of non-void-returning functions so
long as the return type is trivially destructible.
or:
(b) Have in the standard library a "dummy_prvalue" template function
such as what I've written, with well-defined behaviour so long as you
don't try to access the return value.
Received on 2023-07-15 13:03:16