原文链接:

OOP51-CPP. Do not slice derived objects


一个继承自基类的对象通常包含额外扩展基类的成员变量。当通过值赋值或者拷贝派生类型的对象到基类对象时,那些额外的成员变量并不会被拷贝,因为基类没有充足的空间来存储它们。这类动作通常称为 切片 对象,因为额外的成员从结果对象中“切”出去了。

不要初始化通过一个派生类型的对象来初始化一个基类对象,除了通过引用、指针或类指针抽象类(pointer-like abstractions)(例如 std::unique_ptr 或者 std::shared_ptr)。

不合规代码示例

在这个不合规代码示例中,一个派生类 Manager 对象被值传递到一个接受基类 Employee 函数中。因此,Manager 对象被切片,导致信息丢失。当 print() 函数被调用时,造成非预期行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <string>

class Employee {
std::string name;

protected:
virtual void print(std::ostream &os) const {
os << "Employee: " << get_name() << std::endl;
}

public:
Employee(const std::string &name) : name(name) {}
const std::string &get_name() const { return name; }
friend std::ostream &operator<<(std::ostream &os, const Employee &e) {
e.print(os);
return os;
}
};

class Manager : public Employee {
Employee assistant;

protected:
void print(std::ostream &os) const override {
os << "Manager: " << get_name() << std::endl;
os << "Assistant: " << std::endl << "\t" << get_assistant() << std::endl;
}

public:
Manager(const std::string &name, const Employee &assistant) : Employee(name), assistant(assistant) {}
const Employee &get_assistant() const { return assistant; }
};

void f(Employee e) {
std::cout << e;
}

int main() {
Employee coder("Joe Smith");
Employee typist("Bill Jones");
Manager designer("Jane Doe", typist);

f(coder);
f(typist);
f(designer);
}

当接受 designer 形参的 f() 被调用时, f() 中正真的实参被切片,信息丢失。当对象 e 被打印时, Employee::print() 被调用,而不是 Manager::print() ,造成如下输出:

Employee: Jane Doe

兼容方案(指针)

这个兼容方案使用和不兼容代码示例相同的类型定义,修改了 f() 的定义来接受指向对象的原生指针,消除了切片问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Remainder of code unchanged...

void f(const Employee *e) {
if (e) {
std::cout << *e;
}
}

int main() {
Employee coder("Joe Smith");
Employee typist("Bill Jones");
Manager designer("Jane Doe", typist);

f(&coder);
f(&typist);
f(&designer);
}

这个兼容方案关于 f() 的实现也符合 EXP34-C. Do not dereference null pointers 。基于改定义,程序正确输出如下。

1
2
3
4
5
Employee: Joe Smith
Employee: Bill Jones
Manager: Jane Doe
Assistant:
``Employee: Bill Jones

兼容方案(引用)

一种改进的兼容方案,使用引用而不是指针,从而不需要判断传入 f() 空指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ... Remainder of code unchanged ...

void f(const Employee &e) {
std::cout << e;
}

int main() {
Employee coder("Joe Smith");
Employee typist("Bill Jones");
Manager designer("Jane Doe", typist);

f(coder);
f(typist);
f(designer);
}

兼容方案(不可拷贝)

前两种兼容依赖于 EmployeeManager 类型的消费者以兼容期望的类型层级的方式声明。通过移除拷贝初始化(copy-initialize)一个继承自 Noncopyable 的对象的能力,这个兼容方案确保消费者不能意外地将对象切片。如果尝试拷贝初始化,由于在 f() 的最初定义中,该程序是病态的(ill-formed),诊断将被抛出。然而,这种解决方案也限制 Manager 对象试图拷贝初始化它的 Employee 对象。这微妙地改变了类型层次的语义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
#include <string>

class Noncopyable {
Noncopyable(const Noncopyable &) = delete;
void operator=(const Noncopyable &) = delete;

protected:
Noncopyable() = default;
};

class Employee : Noncopyable {
// Remainder of the definition is unchanged.
std::string name;

protected:
virtual void print(std::ostream &os) const {
os << "Employee: " << get_name() << std::endl;
}

public:
Employee(const std::string &name) : name(name) {}
const std::string &get_name() const { return name; }
friend std::ostream &operator<<(std::ostream &os, const Employee &e) {
e.print(os);
return os;
}
};

class Manager : public Employee {
const Employee &assistant; // Note: The definition of Employee has been modified.

// Remainder of the definition is unchanged.
protected:
void print(std::ostream &os) const override {
os << "Manager: " << get_name() << std::endl;
os << "Assistant: " << std::endl << "\t" << get_assistant() << std::endl;
}

public:
Manager(const std::string &name, const Employee &assistant) : Employee(name), assistant(assistant) {}
const Employee &get_assistant() const { return assistant; }
};

// If f() were declared as accepting an Employee, the program would be
// ill-formed because Employee cannot be copy-initialized.
void f(const Employee &e) {
std::cout << e;
}

int main() {
Employee coder("Joe Smith");
Employee typist("Bill Jones");
Manager designer("Jane Doe", typist);

f(coder);
f(typist);
f(designer);
}

不兼容代码示例

这个不兼容代码示例使用了与前面不兼容代码示例中相同的 EmployeeManager 类定义,试图在 std::vector 中存储 Employee 对象。然而, 由于 std::vector 需要同类的元素列表,因此切片发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string>
#include <vector>

void f(const std::vector<Employee> &v) {
for (const auto &e : v) {
std::cout << e;
}
}

int main() {
Employee typist("Joe Smith");
std::vector<Employee> v{typist, Employee("Bill Jones"), Manager("Jane Doe", typist)};
f(v);
}

兼容方案

这个兼容方案使用了 std::unique_ptr 对象的 vector,来消除切片问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string>
#include <vector>

void f(const std::vector<Employee> &v) {
for (const auto &e : v) {
std::cout << e;
}
}

int main() {
Employee typist("Joe Smith");
std::vector<Employee> v{typist, Employee("Bill Jones"), Manager("Jane Doe", typist)};
f(v);
}

Risk Assessment

Slicing results in information loss, which could lead to abnormal program execution or denial-of-service attacks.

Rule Severity Likelihood Remediation Cost Priority Level
OOP51-CPP Low Probable Medium P4 L3

Automated Detection

Tool Version Checker Description
CodeSonar 6.2p0 LANG.CAST.OBJSLICE Object Slicing
Helix QAC 2022.1 C++3072
Parasoft C/C++test 2021.2 CERT_CPP-OOP51-a Avoid slicing function arguments / return value
Polyspace Bug Finder R2021b CERT C++: OOP51-CPP Checks for object slicing (rule partially covered)
PRQA QA-C++ 4.4 3072
PVS-Studio 7.17 V1054

Search for other vulnerabilities resulting from the violation of this rule on the CERT website.

SEI CERT C++ Coding Standard ERR61-CPP. Catch exceptions by lvalue reference CTR56-CPP. Do not use pointer arithmetic on polymorphic objects
SEI CERT C Coding Standard EXP34-C. Do not dereference null pointers

Bibliography

[Dewhurst 2002] Gotcha #38, “Slicing”
[ISO/IEC 14882-2014] Subclause 12.8, “Copying and Moving Class Objects”
[Sutter 2000] Item 40, “Object Lifetimes—Part I”

img img img