Usage of noexcept in derived classes

一曲冷凌霜 提交于 2019-12-11 03:48:52

问题


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

回答1:


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




回答2:


After lot of researches around the Internet, I found that the only "acceptable" solution is to implement my own traits, dealing only with the noexcept-ness. The ability to construct an object or not is ignored, as this is the reason of the issue with abstract classes.

Here is what I implemented in my library. Explanations are given next to code sample.
(Note: The az:: namespace and AZ_ prefixes identify the material provided by my library.)

#include <cstdlib>
#include <iostream>

// --- Default traits ---

namespace az
{
    template < typename CLASS > struct has_noexcept_default_constructor { static const bool value = false; };
    template < typename CLASS > struct has_noexcept_copy_constructor { static const bool value = false; };
    template < typename CLASS > struct has_noexcept_move_constructor { static const bool value = false; };
    template < typename CLASS > struct has_noexcept_copy_operator { static const bool value = false; };
    template < typename CLASS > struct has_noexcept_move_operator { static const bool value = false; };
}

// --- Helper macros ---

#define AZ_SET_NOEXCEPT_DEFAULT_CONSTRUCTOR( CLASS, VALUE ) \
template <> struct az::has_noexcept_default_constructor < class CLASS > { static const bool value = ( VALUE ); }

#define AZ_SET_NOEXCEPT_COPY_CONSTRUCTOR( CLASS, VALUE ) \
template <> struct az::has_noexcept_copy_constructor < class CLASS > { static const bool value = ( VALUE ); }

#define AZ_SET_NOEXCEPT_MOVE_CONSTRUCTOR( CLASS, VALUE ) \
template <> struct az::has_noexcept_move_constructor < class CLASS > { static const bool value = ( VALUE ); }

#define AZ_SET_NOEXCEPT_COPY_OPERATOR( CLASS, VALUE ) \
template <> struct az::has_noexcept_copy_operator < class CLASS > { static const bool value = ( VALUE ); }

#define AZ_SET_NOEXCEPT_MOVE_OPERATOR( CLASS, VALUE ) \
template <> struct az::has_noexcept_move_operator < class CLASS > { static const bool value = ( VALUE ); }

// --- Foo class ---

AZ_SET_NOEXCEPT_DEFAULT_CONSTRUCTOR ( Foo, true );
AZ_SET_NOEXCEPT_MOVE_CONSTRUCTOR ( Foo, true );

class Foo
{
public:
    Foo () noexcept ( az::has_noexcept_default_constructor < Foo >::value ) {}
    Foo ( Foo&& other ) noexcept ( az::has_noexcept_move_constructor < Foo >::value ) {}
    virtual ~Foo () = 0;
};
Foo::~Foo () {}

// --- Bar class ---

AZ_SET_NOEXCEPT_DEFAULT_CONSTRUCTOR ( Bar, az::has_noexcept_default_constructor < Foo >::value );

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

// --- Tests ---

int main ()
{
    std::cout << std::boolalpha;

    bool fooHasNedc = az::has_noexcept_default_constructor < Foo >::value;
    bool fooHasNecc = az::has_noexcept_copy_constructor < Foo >::value;
    bool fooHasNemc = az::has_noexcept_move_constructor < Foo >::value;

    bool fooIsNtdc = std::is_nothrow_default_constructible < Foo >::value;
    bool fooIsNtcc = std::is_nothrow_copy_constructible < Foo >::value;
    bool fooIsNtmc = std::is_nothrow_move_constructible < Foo >::value;

    std::cout << "Foo has noexcept def/copy/move constructors: " << fooHasNedc << "   " << fooHasNecc << "  " << fooHasNemc << '\n';
    std::cout << "Foo is nothrow def/copy/move constructible:  " << fooIsNtdc << "  " << fooIsNtcc << "  " << fooIsNtmc << '\n';
    std::cout << std::endl;

    bool barHasNedc = az::has_noexcept_default_constructor < Bar >::value;
    bool barHasNecc = az::has_noexcept_copy_constructor < Bar >::value;
    bool barHasNemc = az::has_noexcept_move_constructor < Bar >::value;

    bool barIsNtdc = std::is_nothrow_default_constructible < Bar >::value;
    bool barIsNtcc = std::is_nothrow_copy_constructible < Bar >::value;
    bool barIsNtmc = std::is_nothrow_move_constructible < Bar >::value;

    std::cout << "Bar has noexcept def/copy/move constructors: " << barHasNedc << "   " << barHasNecc << "  " << barHasNemc << '\n';
    std::cout << "Bar is nothrow def/copy/move constructible:  " << barIsNtdc << "   " << barIsNtcc << "  " << barIsNtmc << '\n';
    std::cout << std::endl;

    return EXIT_SUCCESS;
}

Output:

Foo has noexcept def/copy/move constructors: true   false  true
Foo is nothrow def/copy/move constructible:  false  false  false

Bar has noexcept def/copy/move constructors: true   false  false
Bar is nothrow def/copy/move constructible:  true   false  false

The default traits provide a default implementation for throwing constructors & assignment operators.

Helper macros make specialized traits implementation very simple. They are used only once in the header file. Then, the trait is used in both .hpp and .cpp files. This way, modifying a noexcept value in the trait (through the macro) updates both declaration and definition (ease maintenability).

As you can see, the noexcept specifier of the Foo default constructor is no more hidden by its non-constructible aspect.

This code perfecty compile under VisualStudio 2015 and clang++.
g++ generates the following error (I'm sure it can be fixed in a way or another ^^):

main.cpp:19:24: error: specialization of 'template<class CLASS> struct az::has_noexcept_default_constructor' in different namespace [-fpermissive]
 template <> struct az::has_noexcept_default_constructor < class CLASS > { static const bool value = ( VALUE ); }
                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Hope this could help people facing the same issue. :)



来源:https://stackoverflow.com/questions/40441994/usage-of-noexcept-in-derived-classes

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!