原文链接:
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 许可协议。转载请注明出处!并保留本声明。感谢您的阅读和支持!