Search code examples
c++c++11stdtraitsnoexcept

Usage of noexcept in derived classes


I encounter an issue while using the noexcept specifier on derived classes, more precisely when the parent is an abstract class (has protected constructors).

Hereafter is an example of the way I declare my classes.

  • With a public constructor in the base class: Everything is ok.
  • Same code with protected and the derived class is no more "nothrow movable".

Do I miss something? Is std::is_nothrow_move_constructible the correct traits to use in derived class declarations or should I use something else?

#include <cstdlib>
#include <iostream>

class BaseOk
{
public:
    BaseOk ( BaseOk&& other ) noexcept {}
};

class BaseNok
{
protected:
    BaseNok ( BaseNok&& other ) noexcept {}
};

class ChildOk : public BaseOk
{
public:
    ChildOk ( ChildOk&& other ) noexcept ( std::is_nothrow_move_constructible < BaseOk >::value )
        : BaseOk ( std::move ( other ) ) {}
};

class ChildNok : public BaseNok
{
public:
    ChildNok ( ChildNok&& other ) noexcept ( std::is_nothrow_move_constructible < BaseNok >::value )
        : BaseNok ( std::move ( other ) ) {}
};

int main ()
{
    std::cout << std::boolalpha;
    std::cout << "Is BaseOk   move constructible?         " << std::is_move_constructible < BaseOk >::value << '\n';
    std::cout << "Is ChildOk  move constructible?         " << std::is_move_constructible < ChildOk >::value << '\n';

    std::cout << '\n';
    std::cout << "Is BaseOk   nothrow move constructible? " << std::is_nothrow_move_constructible < BaseOk >::value << '\n';
    std::cout << "Is ChildOk  nothrow move constructible? " << std::is_nothrow_move_constructible < ChildOk >::value << '\n';

    std::cout << '\n';
    std::cout << "Is BaseNok  move constructible?         " << std::is_move_constructible < BaseNok >::value << '\n';
    std::cout << "Is ChildNok move constructible?         " << std::is_move_constructible < ChildNok >::value << '\n';

    std::cout << '\n';
    std::cout << "Is BaseNok  nothrow move constructible? " << std::is_nothrow_move_constructible < BaseNok >::value << '\n';
    std::cout << "Is ChildNok nothrow move constructible? " << std::is_nothrow_move_constructible < ChildNok >::value << '\n';
    std::cout << std::endl;

    return EXIT_SUCCESS;
}

Output:

 Is BaseOk   move constructible?         true
 Is ChildOk  move constructible?         true

 Is BaseOk   nothrow move constructible? true
 Is ChildOk  nothrow move constructible? true

 Is BaseNok  move constructible?         false
 Is ChildNok move constructible?         true

 Is BaseNok  nothrow move constructible? false
 Is ChildNok nothrow move constructible? false

___ EDIT ____________________________________________________________

After searching around for a while, and regarding to the andswer of Oleg Bogdanov, it unfortunately seems not possible to combine protected constructors with usage of noexcept ( is_nothrow_... ).

I was writting abstract classes and declared constructors protected for documentation purposes only. Now, constructors are back to public but I'm facing another problem:

As an abstract class cannot be instanciated, std::is_nothrow_move_constructible<BaseClass> returns false and all derived classes can never be tagged as not throwing exceptions even if they are not.

See example below:

#include <cstdlib>
#include <iostream>

class Foo
{
public:
    Foo ( Foo&& other ) noexcept {}
    virtual ~Foo () = 0;  // Removing '= 0' makes both outputs print 'true'.
};
Foo::~Foo () {}

class Bar : public Foo
{
public:
    Bar ( Bar&& other ) noexcept ( std::is_nothrow_move_constructible < Foo >::value )
        : Foo ( std::move ( other ) ) {}
};

int main ()
{
    std::cout << std::boolalpha;
    std::cout << "Foo: " << std::is_nothrow_move_constructible < Foo >::value << '\n';
    std::cout << "Bar: " << std::is_nothrow_move_constructible < Bar >::value << '\n';

    return EXIT_SUCCESS;
}

Output:

Foo: false
Bar: false

Solution

  • When evaluating to true is_move_constructible works exactly like is_constructible, which in turn says that

    T is an object or reference type and the variable definition T obj(std::declval()...); is well-formed

    My guess is that in your case, definition BaseNok obj(...) is not really well-formed, because you neither have public default ctor (its implicitly removed) nor any other accessible ctor (protected is not), thus it evals to false. (The definition of well-formeness itself is arguable though)

    ChildNok is still move_constructible because you made its move ctor public, other cases eval to false exactly because std::is_move_constructible < BaseNok >::value is already false


    Edit: As for edited question, note section of is_constructible mentions

    In many implementations, is_nothrow_constructible also checks if the destructor throws because it is effectively noexcept(T(arg))

    When you keep your destructor pure virtual, it probably fails the check.

    I am personally not sure if it's oversight or by-design of type traits, some issues are covered in LWG issue 2116

    I'm not proposing a scalable solution here, but why would not you mark your derived classes unconditionally noexcept for now, given that base is noexcept() too