原文链接:
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
| 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
| 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); }
|
兼容方案(不可拷贝)
前两种兼容依赖于 Employee
和 Manager
类型的消费者以兼容期望的类型层级的方式声明。通过移除拷贝初始化(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 { 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; 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(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); }
|
不兼容代码示例
这个不兼容代码示例使用了与前面不兼容代码示例中相同的 Employee
和 Manager
类定义,试图在 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
Search for other vulnerabilities resulting from the violation of this rule on the CERT website.
Bibliography
本文标题:OOP51-CPP-不要切分派生对象
文章作者:xwnb
发布时间:2022-04-01
最后更新:2023-04-17
原始链接:https://xwnb.github.io/posts/1084595410/
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!并保留本声明。感谢您的阅读和支持!