I found a strange behavior of the noexcept
operator in C++14.
The following code compiles well by both gcc and clang (with --std=c++14 option).
// test.cpp
#include <iostream>
#include <type_traits>
#if 1
#define TESTREF(X) X&&
#else
#define TESTREF(X) X const&
#endif
template <class F, class... Args>
struct is_noexcept_callable
: public std::conditional_t<noexcept(std::declval<F>()(std::declval<Args>()...)), std::true_type, std::false_type> {};
template <
class F,
std::enable_if_t<is_noexcept_callable<F,int>::value,int> = 0
>
int evalInt(int x, TESTREF(F) f) noexcept
{
return static_cast<int>(f(x));
}
template <
class F,
std::enable_if_t<!is_noexcept_callable<F,int>::value,int> = 0
>
int evalInt(int x, TESTREF(F) f)
{
return static_cast<int>(f(x));
}
int id(int x) noexcept { return x; }
int thrower(int x) { throw(0); }
int main(int argc, char* argv[])
{
std::cout << std::boolalpha
<< noexcept(evalInt(1,id))
<< std::endl;
std::cout << std::boolalpha
<< is_noexcept_callable<decltype(thrower), int>::value
<< std::endl;
}
Executing the result program, I however got different results depending on compilers:
$ g++ --std=c++14 test.cpp
$ ./a.out
true
false
$ clang++ --std=c++14 test.cpp
$ ./a.out
false
false
I am not sure which is correct according to the standard.
More strangely, if I change the 5th line in the code above to #if 0
then gcc compiles the code into another differnt program:
$ ./a.out
true
true
As you see, the second value is changed.
However, it depends only on the noexcept
specification of thrower
function that the macro doesn't touch.
Is there any reasonable explanation to this, or is it just a bug?
Edit
The result is obtained with GCC 7.4.0 and clang 6.0.0 in Ubuntu 18.04 (64bit) package repository.
noexcept specifier(C++11) noexcept operator(C++11) Dynamic exception specification(until C++17) [edit] Specifies whether a function could throw exceptions.
In contrast, noexcept(false) means that the function may throw an exception. The noexcept specification is part of the function type but can not be used for function overloading. There are two good reasons for the use of noexcept: First, an exception specifier documents the behaviour of the function.
noexcept is for compiler performance optimizations in the same way that const is for compiler performance optimizations. That is, almost never. noexcept is primarily used to allow "you" to detect at compile-time if a function can throw an exception.
I can only reproduce this bug in GCC before version 8. The difference in behavior is due to the noexcept
specifier being part of the function type in GCC 7's C++14 version (but not Clang's), although this is a C++17 feature. This can be seen if we add partial specializations of is_noexcept_callable
:
template <class... Args>
struct is_noexcept_callable<int(&)(int), Args...>
: public std::false_type {};
template <class... Args>
struct is_noexcept_callable<int(int), Args...>
: public std::false_type {};
This suddenly yields two false
s: GCC retains the noexcept
attribute on function types, but explicitly ignores them during template argument deduction, so that the above specializations are selected, despite error messages showing noexcept
if we remove definitions:
prog.cc:30:5: note: template argument deduction/substitution failed:
prog.cc:28:22: error: incomplete type 'is_noexcept_callable<int (&)(int) noexcept, int>' used in nested name specifier
TESTREF
's definition affect is_noexcept_callable
?The second part of your question is more subtle. Here, the issue is that is_noexcept_callable
is already instantiated with the relevant type int(int) [noexcept]
before you use it in main
, but it has noexcept attached, so that the result of is_noexcept_callable<int(int), int>::value
is fixed to true.
decltype(id)
is int(int) [noexcept]
, where [noexcept]
is my notation to express GCC's transient exception-specification attached to the function type. Thus evalInt(1,id)
causes instantiation of
is_noexcept_callable<F,int>
where F = int(&)(int) [noexcept]
when TESTREF = X&&
andF = int(int) [noexcept]
when TESTREF = X const&
So when you disable the first branch of your if-directive, then is_noexcept_callable<int(int),int>::value == true
holds after noexcept(evalInt(1,id))
is processed, because id
is noexcept and this propagates down the instantiation chain.
Consequently, the following prints two falses:
int main(int argc, char* argv[])
{
std::cout << std::boolalpha
<< noexcept(evalInt(1,thrower))
<< std::endl;
std::cout << std::boolalpha
<< is_noexcept_callable<decltype(thrower), int>::value
<< std::endl;
}
Demo: https://wandbox.org/permlink/YXDYfXwtEwMQkryD
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With