3

I have several methods that

  • have to be marked noexcept
  • must not be marked noexcept

How to write unit tests that check the method for being marked noexcept properly?

Reason: to ensure that in the future those properties are not changed during refactoring by other developers or an evil version of myself.

Currently, I use CMake/CTest and add hand-written executables to the test suite.

Anton Menshov
  • 2,266
  • 14
  • 34
  • 55

2 Answers2

6

noexcept is also an operator. You can use it in a static assertion:

void foo() noexcept { }
void bar() { }

static_assert(noexcept(foo())); // OK
static_assert(noexcept(bar())); // Will fail

If it's a member function, then:

struct S {
    void foo() noexcept { }
};

static_assert(noexcept(S().foo()));

There's no function call or anything performed. The noexcept operator only checks the expression, it doesn't actually evaluate it.

To require that a function must not be noexcept, just use !noexcept(bar()).

Nikos C.
  • 50,738
  • 9
  • 71
  • 96
  • 1
    Thanks a lot! Feel a bit embarrassed since I could have got it even from [cppreference.com](https://en.cppreference.com/w/cpp/language/noexcept). You opened my eyes. – Anton Menshov Jun 08 '19 at 21:22
  • 2
    Note that you can also use `noexcept(declval().foo())` in place of `noexcept(S().foo())` if `S` is not default constructible – alter_igel Jun 08 '19 at 22:12
  • 2
    The `std::declval` approach has another benefit: it allows you to place the `static_assert` _within_ the class, say right after the function declaration, as `S` needn’t be complete for `std::declval()` to be valid (and the operand expr of `noexcept` will be unevaluated). I.e., you may place `static_assert(noexcept(std::declval().foo()))` in the definition of the class, say, immediately below the declaration(/definition) of `foo()`. – dfrib Jun 08 '19 at 22:26
  • ... Particularly, from [declval]/3: _”The template parameter `T` of `declval` may be an incomplete type.”_. – dfrib Jun 08 '19 at 22:33
2

Since you are using C++17, the noexcept-ness of a function is part of its type.

To check the type of the arguments and return type at the same type, you can just use a simple std::is_same:

void foo(int, int) noexcept;
void bar(long, float);
struct S {
    void foo(int, int) noexcept;
    void bar(long, float);
};

static_assert(std::is_same_v<decltype(foo), void(int, int) noexcept>);
static_assert(std::is_same_v<decltype(bar), void(long, float)>);
static_assert(std::is_same_v<decltype(&S::foo), void(S::*)(int, int) noexcept>);
static_assert(std::is_same_v<decltype(&S::bar), void(S::*)(long, float)>);

You can also use template argument deduction to see if a function type is noexcept without having to check noexcept(std::declval<S>().foo(std::declval<arg_1_t>(), std::declval<arg_2_t>())):

// Arguments, return type and type of the class that has the member function are
// all deduced, but this overload is only called if noexcept
template<class RetT, class T, class... Args>
constexpr bool is_noexcept_function(RetT(T::*)(Args...) noexcept) {
    return true;
}
// And this one is called if not noexcept
template<class RetT, class T, class... Args>
constexpr bool is_noexcept_function(RetT(T::*)(Args...)) {
    return false;
}

static_assert(is_noexcept_function(&S::foo));
static_assert(!is_noexcept_function(&S::bar));

The full solution is kind of long to work for const (and other) qualified member functions, as well as functions with variadic parameters:

#include <type_traits>

// Check if a regular function is noexcept
template<class Ret, class... Args>
constexpr std::false_type is_noexcept_function(Ret(Args...)) noexcept {
    return {};
}

template<class Ret, class... Args>
constexpr std::true_type is_noexcept_function(Ret(Args...) noexcept) noexcept {
    return {};
}

// Check if a regular function with C-style variadic arguments is noexcept
template<class Ret, class... Args, bool is_noexcept>
constexpr std::false_type is_noexcept_function(Ret(Args......)) noexcept {
    return {};
}

template<class Ret, class... Args, bool is_noexcept>
constexpr std::true_type is_noexcept_function(Ret(Args......) noexcept) noexcept {
    return {};
}

// Check if a member function is noexcept
#define DEFINE_IS_NOEXCEPT_FUNCTION_FOR_METHOD(QUALIFIER) \
template<class Ret, class T, class... Args> \
constexpr std::false_type is_noexcept_function(Ret(T::*)(Args...) QUALIFIER) noexcept { \
    return {}; \
} \
template<class Ret, class T, class... Args> \
constexpr std::true_type is_noexcept_function(Ret(T::*)(Args...) QUALIFIER noexcept) noexcept { \
    return {}; \
} \
template<class Ret, class T, class... Args, bool is_noexcept> \
constexpr std::false_type is_noexcept_function(Ret(T::*)(Args......) QUALIFIER) noexcept { \
    return {}; \
} \
template<class Ret, class T, class... Args, bool is_noexcept> \
constexpr std::true_type is_noexcept_function(Ret(T::*)(Args......) QUALIFIER noexcept) noexcept { \
    return {}; \
}

#define DEFINE_IS_NOEXCEPT_FUNCTION_FOR_METHOD_VALUE_CLASS(VALUE_CLASS) \
    DEFINE_IS_NOEXCEPT_FUNCTION_FOR_METHOD(VALUE_CLASS) \
    DEFINE_IS_NOEXCEPT_FUNCTION_FOR_METHOD(const VALUE_CLASS) \
    DEFINE_IS_NOEXCEPT_FUNCTION_FOR_METHOD(volatile VALUE_CLASS) \
    DEFINE_IS_NOEXCEPT_FUNCTION_FOR_METHOD(const volatile VALUE_CLASS)

DEFINE_IS_NOEXCEPT_FUNCTION_FOR_METHOD_VALUE_CLASS()
DEFINE_IS_NOEXCEPT_FUNCTION_FOR_METHOD_VALUE_CLASS(&)
DEFINE_IS_NOEXCEPT_FUNCTION_FOR_METHOD_VALUE_CLASS(&&)

#undef DEFINE_IS_NOEXCEPT_FUNCTION_FOR_METHOD
#undef DEFINE_IS_NOEXCEPT_FUNCTION_FOR_METHOD_VALUE_CLASS


// Usage example

void foo(int, int) noexcept;
void bar(long, float);
struct S {
    void foo(int, int) const noexcept;
    void bar(long, float) &&;
};

static_assert(is_noexcept_function(foo));
static_assert(!is_noexcept_function(bar));
static_assert(is_noexcept_function(&S::foo));
static_assert(!is_noexcept_function(&S::bar));

(Most of the time you can get away with just supporting RetT(Args...), RetT(T::*)(Args...) and RetT(T::*)(Args...) const, as you rarely see variadic functions and value category qualified member functions in the wild)

This won't work with templated or overloaded functions/member functions. This is because the noexcept-ness might depend on the template parameters or the overloaded argument types. You can manually provide the template arguments (e.g. is_noexcept(add<int>) and !is_noexcept(add<std::string>)) or fall back to the noexcept operator and std::declval.

Artyer
  • 31,034
  • 3
  • 47
  • 75