原文链接:

OOP55-CPP. Do not use pointer-to-member operators to access nonexistent members


成员指针运算符 .* 和 ->* 被用于获取一个对象或者函数,像是访问底层对象的成员一样。例如,下述代码和调用对象 o 的成员函数 f() 功能上是等价的。

1
2
3
4
5
6
7
8
9
10
11
struct S {
void f() {}
};

void func() {
S o;
void (S::*pm)() = &S::f;

o.f();
(o.*pm)();
}

o.f() 的调用方式在编译器通过类成员访问来查找对象 oS::f() 的函数地址。(o.*pm)() 的调用方式通过成员指针运算符 .* 来调用 pm 指向的地址上的函数。两种情况中,对象 o 都是成员函数 S::f() 的隐式 this 对象。

C++ 标准, [expr.mptr.oper], 段 4 [ISO/IEC 14882-2014], 陈述如下:

Abbreviating pm-expression.*cast-expression as E1.*E2, E1 is called the object expression. If the dynamic type of E1 does not contain the member to which E2 refers, the behavior is undefined.

缩写成 E1.*E2 的 pm-expression.*cast-expression 被称为对象表达式。如果 E1 的动态类型不包含 E2 引用的成员,则该行为是未定义的。

形如 E1->*E2 的成员指针表达式被转换为等价形式,(*(E1)).*E2, 所以其中任一形式的成员指针表达式的使用均表现为 undefined behavior.

进一步地, C++ 标准, [expr.mptr.oper], 段 6, 部分陈述如下:

If the second operand is the null pointer to member value, the behavior is undefined.

如果第二个操作对象是空指针,该行为是未定义的。

在第一个操作数的动态类型不包含第二个操作数所引用的成员时,不要使用成员指针表达式,包括第二个操作数是一个空指针成员。

不合规代码示例

在这个不合规的代码示例中,从 D::g 中获取的一个成员指针对象被向上转换为 B::* 。当在动态类型为 D 的对象上调用时,成员指针调用是良好的。然而,若动态类型的潜在对象是 B,则导致 未定义行为,undefined behavior.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct B {
virtual ~B() = default;
};

struct D : B {
virtual ~D() = default;
virtual void g() { /* ... */ }
};

void f() {
B *b = new B;

// ...

void (B::*gptr)() = static_cast<void(B::*)()>(&D::g);
(b->*gptr)();
delete b;
}

合规方案

在这个合规方案中,向上转换被移除,使原始代码表现为 病态,ill-formed ,突出 B::g()不存在的潜在问题。该兼容方案假设程序员的意图是要使用底层对象正确的动态类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct B {
virtual ~B() = default;
};

struct D : B {
virtual ~D() = default;
virtual void g() { /* ... */ }
};

void f() {
B *b = new D; // Corrected the dynamic object type.

// ...
void (D::*gptr)() = &D::g; // Moved static_cast to the next line.
(static_cast<D *>(b)->*gptr)();
delete b;
}

不合规代码示例

在这个不合规的代码示例中,传入空成员指针作为成员指针表达式的第二个操作数,造成 未定义行为,undefined behavior.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct B {
virtual ~B() = default;
};

struct D : B {
virtual ~D() = default;
virtual void g() { /* ... */ }
};

static void (D::*gptr)(); // Not explicitly initialized, defaults to nullptr.
void call_memptr(D *ptr) {
(ptr->*gptr)();
}

void f() {
D *d = new D;
call_memptr(d);
delete d;
}

合规方案

在这个合规方案中,gptr 被正确初始化为一个合法的成员指针,而不是默认的 nullptr.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct B {
virtual ~B() = default;
};

struct D : B {
virtual ~D() = default;
virtual void g() { /* ... */ }
};

static void (D::*gptr)() = &D::g; // Explicitly initialized.
void call_memptr(D *ptr) {
(ptr->*gptr)();
}

void f() {
D *d = new D;
call_memptr(d);
delete d;
}

Risk Assessment

Rule Severity Likelihood Remediation Cost Priority Level
OOP55-CPP High Probable High P6 L2

Automated Detection

Tool Version Checker Description
Astrée 20.10 overflow_upon_dereference invalid_function_pointer
Axivion Bauhaus Suite 7.2.0 CertC++-OOP55
Helix QAC 2022.1 C++2810, C++2811, C++2812, C++2813, C++2814
Klocwork 2022.1 CERT.OOP.PTR_MEMBER.NO_MEMBER
Parasoft C/C++test 2021.2 **CERT_CPP-OOP55-a ** A cast shall not convert a pointer to a function to any other pointer type, including a pointer to function type
Parasoft Insure++ Runtime detection
PRQA QA-C++ 4.4 2810, 2811, 2812, 2813, 2814

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

This rule is a subset of EXP34-C. Do not dereference null pointers.

Bibliography

[ISO/IEC 14882-2014] Subclause 5.5, “Pointer-to-Member Operators”

img img img