警告:不推荐使用隐式复制构造函数的定义

Posted

技术标签:

【中文标题】警告:不推荐使用隐式复制构造函数的定义【英文标题】:Warning: definition of implicit copy constructor is deprecated 【发布时间】:2018-08-15 17:28:55 【问题描述】:

我的 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)具有虚拟析构函数,则派生类应该具有虚拟析构函数。

因此我想最好不要删除虚拟析构函数,而是定义复制构造函数。但是如果我需要定义复制构造函数,也许我还应该定义复制赋值运算符和移动构造函数以及移动赋值运算符。但这对于我的简单异常类来说似乎有点过分了!?

任何想法如何最好地解决这个问题?

【问题讨论】:

这并不是说不会生成复制构造函数,而是说不推荐使用隐式生成行为。从 C++11 开始。您可以添加MyError(MyError const&) = default; 来抑制警告。您还可以摆脱析构函数定义,因为由于基类的析构函数是虚拟的,它将是隐式虚拟的 您不必为派生类中的析构函数手动创建覆盖。虚拟一个基地就足够了。 无法使用常规 clang 或 gcc 进行复制。一定是 clang-cl 的怪癖。尽量不要定义析构函数。 必须将开关 -Wdeprecated 添加到 reproduce it。看起来问题是编译器的配置。您是否使用了一些自定义编译器选项? @Linoliumz 看看我提供的链接,这是一个clang。 【参考方案1】:

您不需要在派生类中显式声明析构函数:

§ 15.4 析构函数 [class.dtor] (强调我的)

析构函数可以声明为虚拟(13.3)或纯虚拟(13.4);如果 该类或任何派生类的任何对象都在 程序,应定义析构函数。如果一个类有一个基类 使用虚拟析构函数,它的析构函数(无论是用户还是 隐式声明)是虚拟的

事实上,在某些情况下,它甚至可能对性能不利,因为显式声明析构函数会阻止隐式生成移动构造函数和移动赋值运算符。

除非您需要在析构函数中做某事,否则最好的做法是省略析构函数的显式声明。

如果您确实需要自定义析构函数,并且确定默认的复制 ctor、复制赋值运算符、移动 ctor 和移动赋值运算符会为您做正确的事情,最好像这样显式默认它们:

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、复制 ctor 和赋值运算符用于处理不同类型的资源(最显着的是指向内存的指针,还有其他的,比如文件描述符和网络套接字仅命名为夫妇)很少做正确的行为。如果程序员认为他需要对类析构函数中文件句柄的关闭进行特殊处理,他肯定想定义应该如何复制或移动这个类。

为了完整起见,以下是经常相关的 5 规则,以及有争议的零规则

五法则

因为存在用户定义的析构函数,复制构造函数, 或复制赋值运算符防止隐式定义移动 构造函数和移动赋值运算符,移动的任何类 语义是可取的,必须声明所有五个特殊成员 功能:

零规则

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

【讨论】:

感谢您的详细解释。我将简单地删除我的析构函数,因为它不需要。 我之所以对删除虚拟析构函数持谨慎态度,是因为我记得 C++ 编译器警告说基类具有虚拟方法但没有虚拟析构函数。但我想这是一个不同的问题。 请注意,如果您要通过指向基址的指针删除对象,仍然需要虚拟析构函数。在您的特定情况下, std::exception 有一个声明为虚拟的构造函数,因此您无需关心它。如果您正在创建一个用户类,并且您拥有该类的基类,那么您肯定应该在基类中声明一个虚拟析构函数。正如有关零规则的链接文章所说,在这种情况下,您应该明确默认所有 5 个特殊成员函数。 我之前的评论应该声明“std::exception 有一个 destructor 声明为虚拟”。没有虚构造函数,说构造函数有错别字。 在阅读您的“构造函数声明为虚拟”时,我发现您实际上的意思是“析构函数声明为虚拟”;-)【参考方案2】:

现在我可以通过简单地删除虚拟析构函数来消除该警告,但我一直认为如果基类(在本例中为 std::runtime_error)具有虚拟析构函数,派生类应该具有虚拟析构函数。

你想错了。如果您在基类中定义一个,无论您是否显式创建它,派生类将始终具有虚拟析构函数。所以删除析构函数将是最简单的解决方案。正如您在documentation for std::runtime_exception 中看到的那样,它也没有提供自己的析构函数,它是编译器生成的,因为基类std::exception 确实具有虚拟 dtor。

但如果您确实需要析构函数,您可以显式添加编译器生成的复制 ctor:

MyError( const MyError & ) = default;

或禁止它使类不可复制:

MyError( const MyError & ) = delete;

赋值运算符也是如此。

【讨论】:

好的,谢谢。由于我正在寻找一个简单的设计并且不需要自定义析构函数,因此我将简单地删除析构函数。【参考方案3】:

注意:对于许多不同的代码也会发生同样的情况,但我在这里写它以防有人收到相同的警告。

在 GCC 版本 6.4 - 9.0 中存在一个错误,其中 在继承自 base_type(类模板)的类型中使用 base_type 的 operator= 和 base_type 的 ctor 的声明实际上并未创建复制/移动 ctor/operators (以无法复制/移动对象的非常意外的编译器错误结束)。

自 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

【讨论】:

【参考方案4】:

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

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

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

【讨论】:

以上是关于警告:不推荐使用隐式复制构造函数的定义的主要内容,如果未能解决你的问题,请参考以下文章

我定义了一个非拷贝构造函数;复制构造函数是不是仍会被隐式定义?

在 C++0x 中,非静态数据成员初始化器会覆盖隐式复制构造函数吗?

函数的隐式声明是啥意思?

爪哇。隐式超级构造函数 Employee() 未定义。必须显式调用另一个构造函数[重复]

c++ 拷贝构造函数与赋值运算符重载函数的区别是

C ++用户定义的空默认构造函数与隐式或默认的默认构造函数