Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange behavior of noexcept specifier in C++14

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.

like image 512
junology Avatar asked Feb 13 '20 05:02

junology


People also ask

What is Noexcept specifier in C++?

noexcept specifier(C++11) noexcept operator(C++11) Dynamic exception specification(until C++17) [edit] Specifies whether a function could throw exceptions.

What does Noexcept false do?

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.

What does Const Noexcept mean?

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.


1 Answers

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 falses: 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

Why does 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&& and
  • F = 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

like image 138
Columbo Avatar answered Sep 29 '22 11:09

Columbo