警告:隐式复制构造函数的定义已被弃用。

45

我在我的C++11代码中收到一个警告,我想正确解决它,但我不知道如何做。我已经创建了自己的异常类,该类派生自std::runtime_error

class MyError : public std::runtime_error
{
public:
    MyError(const std::string& str, const std::string& message)
      : std::runtime_error(message),
        str_(str)
    { }

    virtual ~MyError()
    { }

    std::string getStr() const
    {
        return str_;
    }

  private:
      std::string str_;
};

当我使用/Wall编译该代码时,使用clang-cl编译器会出现以下警告:

warning: definition of implicit copy constructor for 'MyError' is deprecated 
         because it has a user-declared destructor [-Wdeprecated]
因为我在 MyError 中定义了一个析构函数,所以 MyError 将不会生成拷贝构造函数。我不完全明白这是否会导致任何问题...
现在,我可以通过简单地移除虚析构函数来摆脱该警告,但我一直认为,如果基类(在这种情况下是 std::runtime_error)有虚析构函数,则派生类应该有虚析构函数。
因此,我想最好不要移除虚析构函数,而是定义拷贝构造函数。但是,如果我需要定义拷贝构造函数,也许我还应该定义拷贝赋值运算符和移动构造函数以及移动赋值运算符。但是,对于我的简单异常类来说,这似乎有点过度设计了!?
有什么好的解决方法吗?

3
它并没有说不会生成复制构造函数,而是说隐式的生成行为已经被弃用了,这个从C++11开始就已经如此。您可以添加 MyError(MyError const&) = default; 来抑制警告。您还可以摆脱析构函数的定义,因为它将由于基类的析构函数是虚拟的而隐式变成虚拟的。 - Praetorian
2
你不必在派生类中手动创建析构函数的覆盖。在基类中使用虚函数即可。 - Slava
2
无法在常规的clang或gcc中重现。必须是clang-cl的怪癖。尝试不定义析构函数。 - n. m.
1
必须添加 -Wdeprecated 开关才能重现问题。看起来问题是编译器的配置引起的。您使用自定义编译器选项吗? - Marek R
1
@Linoliumz,请查看我提供的链接,这是一个clang编译器。 - Marek R
显示剩余4条评论
4个回答

46
在派生类中,您不需要显式声明析构函数: §15.4 析构函数 [class.dtor] (强调我的) 析构函数可以被声明为虚拟的(13.3)或纯虚拟的(13.4); 如果程序中创建了该类或任何派生类的任何对象,则必须定义析构函数。如果一个类具有一个具有虚拟析构函数的基类,则它的析构函数(无论是用户声明还是隐式声明的)都是虚拟的。
事实上,在某些情况下甚至可能会影响性能,因为显式声明析构函数将防止移动构造函数和移动赋值运算符的隐式生成。
除非在析构函数中需要执行某些操作,否则最好的方法是省略对析构函数的显式声明。
如果您确实需要自定义析构函数,并且确定默认的复制构造函数,复制赋值运算符,移动构造函数和移动赋值运算符可以正确执行,则最好将它们明确设置为默认值,如下所示:
MyError(const MyError&) = default;
MyError(MyError&&) = default;
MyError& operator=(const MyError&) = default;
MyError& operator=(MyError&&) = default;

为什么你会看到这个错误的一些推理,因为在 C++98 中,这是完全有效的代码:

从 C++11 开始,复制构造函数的隐式生成被声明为过时。

§ D.2 复制函数的隐式声明 [depr.impldec]

如果类具有用户声明的复制赋值运算符或用户声明的析构函数,则默认情况下将复制构造函数的隐式定义作为默认项 已经过时。如果类具有用户声明的复制构造函数或用户声明的析构函数,则默认情况下将复制赋值运算符的隐式定义作为默认项(15.4, 15.8)。在此国际标准的将来修订版中,这些隐式定义可能变为已删除 (11.4)。

这段文字背后的原理是众所周知的三大法则。

以下所有引用都来自 cppreference.com: https://en.cppreference.com/w/cpp/language/rule_of_three

三大法则

如果一个类需要用户定义的析构函数、用户定义的复制构造函数或用户定义的复制赋值运算符,那么它几乎肯定需要全部三个。

存在这个经验法则的原因是默认生成的 dtor、复制构造函数和赋值运算符用于处理不同类型的资源(最显著的是指向内存的指针,但也包括其他一些,比如文件描述符和网络套接字)的处理行为很少正确。如果程序员认为他需要特殊处理类析构函数中关闭文件句柄的操作,那么他肯定希望定义如何复制或移动该类。

为了完整起见,下面是通常相关的五大法则和有些争议的零大法则

五大法则

由于用户定义的析构函数、复制构造函数或复制赋值运算符的存在,阻止了移动构造函数和移动赋值运算符隐式定义,任何需要移动语义的类都必须声明所有五个特殊成员函数:

零大法则

具有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符的类应该专门处理所有权 (这遵循单一职责原则)。其他类不应该有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符。


感谢您的详细解释。我会简单地删除我的析构函数,因为它不是必需的。 - Linoliumz
我对删除虚析构函数持谨慎态度的原因是,我记得C++编译器会警告具有虚方法但没有虚析构函数的基类。但我想这是一个不同的问题。 - Linoliumz
3
请注意,如果您将通过指向基类的指针删除对象,则仍需要虚析构函数。在您的特定情况下,std::exception声明了一个虚构造函数,因此您不需要担心它。如果您正在创建一个用户类,并拥有基类,则应在基类中声明虚拟析构函数。正如关于零规则的链接文章所说,在这种情况下,您应明确默认所有5个特殊成员函数。 - divinas
我的上一个评论应该是声明“std::exception有一个声明为虚拟的析构函数”。并没有所谓的虚拟构造函数,说构造函数是笔误。 - divinas
在阅读你的“构造函数声明为虚拟函数”时,我发现你实际上是指“析构函数声明为虚拟函数”;-) - Linoliumz

10
现在我可以通过简单地删除虚析构函数来摆脱那个警告,但是我一直认为如果基类(在这种情况下是std::runtime_error)有虚析构函数,则派生类应该有虚析构函数。
你的想法是错误的。如果您在基类中定义了虚析构函数,那么派生类将始终有一个虚析构函数,无论您是否显式创建它。因此,删除析构函数将是最简单的解决方案。正如您在std::runtime_exception文档中所看到的,它也没有提供自己的析构函数,并且编译器生成了它,因为基类std::exception确实有虚dtor。
但是,如果您确实需要析构函数,您可以显式添加编译器生成的复制构造函数:
MyError( const MyError & ) = default;

或禁止其使类不可复制:
MyError( const MyError & ) = delete;

赋值运算符同理。


好的,谢谢。由于我正在寻找一个简单的设计,而且不需要自定义析构函数,所以我将简单地删除析构函数。 - Linoliumz
这不是工作,我将其恢复为默认设置,仍然无法工作。 - Nicholas Jela
@NicholasJela 定义“不工作”需要详细说明。 - Slava
如果你开始添加默认的复制成员函数,你会意外地禁用移动操作,参见这个概述:https://howardhinnant.github.io/smf.jpg。这意味着你也必须默认移动成员函数。 - undefined

6
注意:虽然代码不同,但是GCC版本6.4-9.0中存在一个错误,即“从base_type继承的类型的using声明,用于base_type的operator=和base_type的ctor,在这种情况下,实际上并没有创建复制/移动ctor/operator”(导致非常意外的编译器错误,无法复制/移动对象)。
自GCC 9.0以来,此错误已得到修复,但会出现此警告。该警告是错误的,不应出现(使用明确声明构造函数/操作符)。
示例代码及其变通方法和GCC版本比较:https://godbolt.org/z/WgIH4c GCC错误报告:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=89381 另一个GCC错误报告:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=92145 此发现的起源:https://github.com/boostorg/spirit/issues/465

1

这将使您的代码在clang 13中编译(但不能正常工作):

   MyError(const MyError&) {};
   MyError(MyError&&) {};
   MyError& operator=(const MyError&) {};
   MyError& operator=(MyError&&) {};

您需要填写适当的代码来实现复制构造函数,
但请注意,您不需要所有4个函数,只需要被调用的那些函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接