我能将C++11中的final关键字应用于POD(标准布局)结构体吗?是否应该这样做?

7
在一个C++项目中,对象很多(具有适当的行为),非面向对象结构较少(只包含数据字段而没有方法)。我希望防止这些结构被意外滥用,比如试图创建一个继承自它的类。
据我所知,因为这些“POD”(普通旧数据)结构没有虚析构函数,如果允许创建派生类对象,则无法通过POD类型的指针正确删除派生类对象。
这似乎是C++11中“final”关键字的一个很好的使用案例,它将一个类或结构标记为不可继承。
然而,我想知道“final”关键字是否会导致结构变成非POD?
我怀疑标准文档可能已经解决了这个问题,但我不够聪明去筛选出非常长的文档以找出答案。任何有用的提示都将受到欢迎。
注意:我不仅仅想知道某些编译器供应商是否通过了编译。编译通过并不能保证:
- 编译后的代码在所有情况下都能正确执行(特别是在更大、更复杂的项目中应用该技术时), - C++标准机构是否打算以这种方式使用。
#include <iostream>
using namespace std;

struct Pod final
{
    int a;
    int b;
    int c;
};

#if 0
class FailsBecauseCannotDeriveFromFinalAnything : public Pod
{
};
#endif

class ContainsSomethingFinalAsMember
{
public:
    ContainsSomethingFinalAsMember() : pod() {}
private:
    Pod pod;
};

int main() 
{
    std::cout << std::is_pod < Pod > :: value << std::endl;
    return 0;
}
2个回答

4
据我理解,由于这些“POD”(plain old data)结构体没有虚析构函数,因此无法通过指向POD类型的指针正确删除派生类对象(如果允许创建该对象)。
但是,如果使用聪明指针对象(例如std :: shared_ptr或具有适当删除器的std :: unique_ptr),则可以实现删除对象。自从聪明指针被标准化以来,很少有借口继续遵循使用delete操作符的不良实践。当然,不应该设计类以兼容删除操作符。每个类接口都应根据其特定的用途进行设计。
不,将每个类设置为final或多态并不是一个好习惯。
然而,我想知道“final”关键字是否会导致结构体变得非POD?
不会,它仍然是POD。POD的要求是标准布局(依次需要没有基类,没有成员之间的访问说明符,没有任何虚拟成员函数)和平凡的特殊成员函数(复制/移动构造函数,赋值运算符,析构函数)。
但是,POD的目的是允许您使用memcpy而不是正确地构造对象,而好的C ++代码应该避免这样做。

1
@CoffeeandCode 智能指针,特别是 unique_ptr,比容器更轻量级。几乎不可能编写一个不从析构函数中调用的异常安全的 delete 表达式,而具有这样析构函数的类往往像智能指针一样。 - Potatoswatter
哦,我同意在可能抛出异常的情况下不应该使用 newdelete。始终使用 RAII 和智能指针。 - RamblingMad
@Potatoswatter:感谢您的回答。这种删除安全性通常是保证的,还是只有在使用std::make_shared<MostDerived>时,才会将正确的删除函数(针对MostDerived)捕获到智能指针中? - rwong
@CoffeeandCode 因为 new 本身通常会抛出异常,所以这种逻辑有点不一致。而且,你必须保证对象的整个生命周期中没有任何异常。在这种情况下,特别是对于 POD 对象,堆栈分配更为合适。 - Potatoswatter
2
@rwong shared_ptr 会一直记住它被初始化时的类型。为了提高效率,使用 make_shared 仍然是一个好主意。对于 unique_ptr,您必须手动指定删除/清理方式,例如捕获无关的 lambda 表达式 [](base *b){delete static_cast<derived *>(b);} - Potatoswatter
显示剩余2条评论

1
标记一个类为final不会改变其POD状态。引用C++11规范:
9 [class]:

一个平凡可复制类是指:

  • 没有非平凡的拷贝构造函数(12.8),
  • 没有非平凡的移动构造函数(12.8),
  • 没有非平凡的拷贝赋值运算符(13.5.3,12.8),
  • 没有非平凡的移动赋值运算符(13.5.3,12.8),并且
  • 有一个平凡析构函数(12.4)。

平凡类是指具有平凡默认构造函数(12.1)且是平凡可复制类的类。 [注: 特别地,一个平凡可复制或平凡类不具有虚函数或虚基类。 ——结束注释]

标准布局类是指:

  • 没有类型为非标准布局类(或该类型数组)或引用的非静态数据成员,
  • 没有虚函数(10.3)和虚基类(10.1),
  • 对于所有非静态数据成员具有相同的访问控制(第11条),
  • 没有非标准布局基类,
  • 在最派生类中没有非静态数据成员,且最多有一个具有非静态数据成员的基类;或没有具有非静态数据成员的基类,并且
  • 没有与第一个非静态数据成员类型相同的基类。

标准布局结构体是指使用class-key structclass-key class定义的标准布局类。...

POD结构体是指既是平凡类又是标准布局类的非联合类,并且没有类型为非POD结构体、非POD联合体(或该类型数组)的非静态数据成员。...

12.1 [class.ctor]

5 ... 如果默认构造函数不是用户提供的,并且:

  • 它所在的类没有虚函数(10.3)和虚基类(10.1),并且
  • 其所在类的所有非静态数据成员都没有花括号或等于号初始化器,并且
  • 其所在类的所有直接基类都有平凡的默认构造函数,并且
  • 其所在类所有非静态数据成员是类类型(或其数组),则这些类都具有平凡的默认构造函数。

否则,该默认构造函数就是非平凡的。

12.4 [class.dtor]

5 ... 如果析构函数不是用户提供的,并且:

  • 析构函数不是虚函数,
  • 其所在类的所有直接基类都有平凡的析构函数,并且
  • 其所在类所有非静态数据成员是类类型(或其数组),则这些类都具有平凡的析构函数。

否则,该析构函数就是非平凡的。

12.8 [class.copy]

如果类X没有虚函数(10.3)和虚基类(10.1),并且每个直接基类子对象的构造函数都是平凡的,那么类X的复制/移动构造函数是平凡的;对于X的每个非静态数据成员,如果其类型为类类型(或其数组),则选择用于复制/移动该成员的构造函数是平凡的。否则,复制/移动构造函数是非平凡的。如果类X没有虚函数(10.3)和虚基类(10.1),并且每个直接基类子对象的赋值运算符都是平凡的,那么类X的复制/移动赋值运算符是平凡的;对于X的每个非静态数据成员,如果其类型为类类型(或其数组),则选择用于复制/移动该成员的赋值运算符是平凡的。否则,复制/移动赋值运算符是非平凡的。
正如您所看到的,POD结构的定义并不考虑类是否具有"class-virt-specifier" final。

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