确定抽象基类的构造函数是否为 noexcept

Determine whether a constructor of an abstract base class is noexcept?

本文关键字:是否 noexcept 构造函数 抽象 基类      更新时间:2023-10-16

在 C++11 及更高版本中,如何确定抽象基类的构造函数是否noexcept?以下方法不起作用:

#include <new>
#include <type_traits>
#include <utility>
struct Base { Base() noexcept; virtual int f() = 0; };
// static assertion fails, because !std::is_constructible<Base>::value:
static_assert(std::is_nothrow_constructible<Base>::value, "");
// static assertion fails, because !std::is_constructible<Base>::value:
static_assert(std::is_nothrow_default_constructible<Base>::value, "");
// invalid cast to abstract class type 'Base':
static_assert(noexcept(Base()), "");
// invalid new-expression of abstract class type 'Base'
static_assert(noexcept(new (std::declval<void *>()) Base()), "");
// cannot call constructor 'Base::Base' directly:
static_assert(noexcept(Base::Base()), "");
// invalid use of 'Base::Base':
static_assert(noexcept(std::declval<Base &>().Base()), "");

一个简单的用法是:

int g() noexcept;
struct Derived: Base {
    template <typename ... Args>
    Derived(Args && ... args)
            noexcept(noexcept(Base(std::forward<Args>(args)...)))
        : Base(std::forward<Args>(args)...)
        , m_f(g())
    {}
    int f() override;
    int m_f;
};

关于如何归档它的任何想法,或者是否可以在不修改抽象基类的情况下进行任何想法?

PS:也欢迎任何对ISO C++缺陷报告或正在进行的工作的引用。

编辑:正如两次指出的那样,默认Derived构造函数具有= default会使noexcept被继承。但这并不能解决一般情况的问题。

[更新:值得跳到编辑部分]

好的,我找到了一个解决方案,即使由于 GCC 中的错误,它无法与所有编译器一起编译(有关更多详细信息,请参阅此问题)。

该解决方案基于继承的构造函数和解析函数调用的方式。
请考虑以下示例:

#include <utility>
#include <iostream>
struct B {
    B(int y) noexcept: x{y} { }
    virtual void f() = 0;
    int x;
};
struct D: public B {
private:
    using B::B;
public:
    template<typename... Args>
    D(Args... args)
    noexcept(noexcept(D{std::forward<Args>(args)...}))
        : B{std::forward<Args>(args)...}
    { }
    void f() override { std::cout << x << std::endl; }
};
int main() {
    B *b = new D{42};
    b->f();
}

我想这很清楚。
无论如何,如果您发现某些内容需要更多详细信息,请告诉我,我很乐意更新答案。
基本思想是,我们可以直接从基类及其构造函数继承noexcept定义,这样我们就不必再在noexcept语句中引用该类。

在这里,您可以看到上面提到的工作示例。

[编辑]

从注释中可以看出,如果基类的构造函数和派生类的构造函数具有相同的签名,则该示例会遇到问题。
感谢Piotr Skotnicki指出这一点。
我将提及这些注释,并将复制并粘贴建议的代码(并在需要时提及作者)。

首先,在这里我们可以看到该示例没有按预期工作(感谢 Piotr Skotnicki 的链接)。
代码与之前发布的代码几乎相同,因此不值得在此处复制和粘贴。
此外,来自同一作者,它遵循一个示例,该示例显示相同的解决方案在某些情况下按预期工作(有关更多详细信息,请参阅注释):

#include <utility>
#include <iostream>
struct B {
    B(int y) noexcept: x{y}
    {
        std::cout << "B: Am I actually called?n";
    }
    virtual void f() = 0;
    int x;
};
struct D: private B {
private:
    using B::B;
public:
    template<typename... Args>
    D(int a, Args&&... args)
    noexcept(noexcept(D{std::forward<Args>(args)...}))
        : B{std::forward<Args>(args)...}
    {
        std::cout << "D: Am I actually called?n";
    }
    void f() override { std::cout << x << std::endl; }
};
int main()
{
    D* d = new D{71, 42};
    (void)d;
}

此外,我提出了一种更具侵入性的替代解决方案,并且基于std::allocator_arg_t所代表的想法,这也是Piotr Skotnicki在评论中提出的相同:

如果派生类的构造函数在重载解析中获胜就足够了。

它遵循此处提到的代码:

#include <utility>
#include <iostream>
struct B {
    B(int y) noexcept: x{y}
    {
        std::cout << "B: Am I actually called?n";
    }
    virtual void f() = 0;
    int x;
};
struct D: public B {
private:
    using B::B;
public:
    struct D_tag { };
    template<typename... Args>
    D(D_tag, Args&&... args)
    noexcept(noexcept(D{std::forward<Args>(args)...}))
        : B{std::forward<Args>(args)...}
    {
        std::cout << "D: Am I actually called?n";
    }
    void f() override { std::cout << x << std::endl; }
};
int main()
{
    D* d = new D{D::D_tag{}, 42};
    (void)d;
}

再次感谢Piotr Skotnicki的帮助和评论,非常感谢。

根据skypjack的回答,一个不需要更改Derived构造函数签名的更好解决方案是将Base的模拟子类定义为Derived的私有类型成员,并在Derived构造函数noexcept规范中使用它的构造:

class Derived: Base {
private:
    struct MockDerived: Base {
        using Base::Base;
        // Override all pure virtual methods with dummy implementations:
        int f() override; // No definition required
    };
public:
    template <typename ... Args>
    Derived(Args && ... args)
            noexcept(noexcept(MockDerived(std::forward<Args>(args)...)))
        : Base(std::forward<Args>(args)...)
        , m_f(g())
    {}
    int f() override { return 42; } // Real implementation
    int m_f;
};

一个天真但工作的例子是引入一个非虚拟基类并通过using指令导出其构造函数。下面是一个示例:

#include<utility>
struct BaseBase {
    BaseBase() noexcept { }
};
struct Base: public BaseBase {
    using BaseBase::BaseBase;
    virtual int f() = 0;
};
struct Derived: public Base {
    template <typename ... Args>
    Derived(Args && ... args)
        noexcept(noexcept(BaseBase(std::forward<Args>(args)...)))
        : Base(std::forward<Args>(args)...)
    { }
    int f() override { }
};
int main() {
    Derived d;
    d.f();
}

我遇到了同样的问题,我找到的解决方案是实现额外的特征。

你可以在这里看看我的帖子。