Effective C++笔记之二十一:One Definition Rule(ODR)

ODR细节有点复杂,跨越各种情况。基本内容如下:

●普通(非模板)的noninline函数和成员函数、noninline全局变量、静态数据成员在整个程序中都应当只定义一次。

●class类型(包括structs和unions)、模板(包括偏特化但是不包括全特化)、inline函数和inline变量在单个编译单元中最多定义一次,并且这些定义应该完全一样。

一个编译单元是源文件预处理后的结果,也就是说,它包含#include指令和宏拓展后的内容。与C语言一样,C++中所有的预处理指令都是以字符#开头,这些指令在编译之前进行处理。

本文将讨论一种违背ODR的典型情况:不同编译单元中包含同名结构体,结构体内函数定义相同,但数据成员不同
MyClass1.h

cpp 复制代码
#ifndef MYCLASS1_H
#define MYCLASS1_H


class MyClass1
{
public:
    MyClass1();
};

#endif // MYCLASS1_H

MyClass1.cpp

cpp 复制代码
#include "MyClass1.h"

struct Point
{
    void setValue(int x, int y)
    {
        this->x = x;
        this->y = y;
    }
    int z;
    int x, y;
};

MyClass1::MyClass1()
{
    Point p;
    p.setValue(0, 0);
}

MyClass2.h

cpp 复制代码
#ifndef MYCLASS2_H
#define MYCLASS2_H


class MyClass2
{
public:
    MyClass2();
};

#endif // MYCLASS2_H

MyClass2.cpp

cpp 复制代码
#include "MyClass2.h"

#include <iostream>

struct Point
{
    void setValue(int x, int y)
    {
        this->x = x;
        this->y = y;
    }
    int x, y;
};

MyClass2::MyClass2()
{
    Point p;
    p.setValue(10, 10);

    std::cout << p.x << std::endl;
}

main.cpp

cpp 复制代码
#include "MyClass1.h"
#include "MyClass2.h"

int main()
{

     MyClass1 cl1;

     MyClass2 cl2;

     return 0;
}

显然,我们的预期打印结果是:10。

本人不同喜欢敲指令,这里IDE使用Qt Creator,Qt版本是5.12.6 MinGW32,编译器为g++。

在Debug模式下,且三个cpp文件的编译顺序是MyClass1.cpp->MyClass2.cpp->main.cpp,如下图所示

实际打印结果却是:0

依然在Debug模式下,编译顺序改为MyClass2.cpp->MyClass1cpp->main.cpp,如下图所示

实际打印结果是预期值:10

在Release模式下,且三个cpp文件的编译顺序是MyClass1.cpp->MyClass2.cpp->main.cpp

实际打印结果也是预期值:10

下面来分析为何和出现上述三种不同的情况,首先要明确以下四点:

1、直接在class {}中定义函数体的函数都是inline的。

2、inline在现代的意义并不是调用处展开函数(是否展开由编译器优化决定),而是允许在多个编译单元(obj文件)中出现相同的符号,链接时不会报符号重定义。如果在class外面定义非inline的函数体(A::A()这样的写法),链接是要报错的。

3、如果inline的符号有出现重复,链接器会随便选择一个。

4、inline的特性被广泛运用在纯hpp文件造轮子,将class的声明和实现都写在头文件中,哪里需要哪里include一下就好,非常方便,无需像原来那样又是h文件又是lib文件,还要保证各种编译条件匹配。

关于inline,详见:Effective C++笔记之十五:inline函数的里里外外

编译器如何决定是否将函数内联呢?

编译器决定是否将函数内联的过程称为内联函数优化。编译器会根据一定的规则和优化策略来决定是否将函数内联。以下是一些关键因素,可以影响编译器的决策:

●函数体积:如果函数体积较小,编译器更可能将其内联。内联函数可以减少函数调用的开销,提高代码执行效率。

●递归函数:递归函数通常不会被内联,因为递归调用可能导致大量的重复代码,从而增加程序的内存占用和执行时间。

●循环中的函数:在循环体内调用的函数也可能被内联。这样可以减少循环中的函数调用开销,提高代码执行效率。

●函数属性:编译器可能会根据函数的属性来决定是否内联。例如,如果函数具有"inline"属性,编译器可能更倾向于将其内联。

●编译器优化级别:编译器的优化级别也会影响其决策。较高的优化级别可能会导致编译器更倾向于内联函数,以提高代码执行效率。

●目标平台:编译器会根据目标平台的特性来决定是否内联函数。例如,在资源受限的平台上,编译器可能更倾向于减少内联,以减少程序的内存占用。

总之,编译器决定是否值得将函数内联取决于多种因素。编译器会根据这些因素以及优化策略来决定是否将函数内联,以提高代码执行效率和减少内存占用。

上面说过inline时是否展开取决于编译器优化,在Debug模式下,g++使用的优化级别是O0(默认选项):不开启优化,方便功能调试。可以明确的是,在O0等级下,内联不会真正发生。 结合前面的现象,在Debug模式下,链接器都选择了较后参与编译的源文件中的setValue函数。

在Release模式下,g++使用的优化级别是O2,O2是常用的Release级别,该级别下几乎执行了所有支持的优化选项,它增加了编译时间,提高了程序的运行速度,会额外打开了一些优化标志,比如-finline-functions。结合前面的现象,在Release模式下,内联真正发生,函数在调用处展开,所以能得到正确结果,尽管如此,由于内联的非强制性,代码这样写依然是有隐患的。
如何判断内联函数有没有在调用处展开呢? 方法见:[C++基础]016_内联函数到底有没有被嵌入到调用处呢?

除了自己写代码要遵循ODR,在使用第三方库时同样要注意,下面是一位网友反馈的情况。
为何同时用两个不同版本的RapidJSON会导致程序崩溃?

rapidjson是一个只包含.h文件就能用的库。意思是,它将所有的类定义写在了头文件里面。这种做法很常见。使得调用者非常方便,只要include 头文件就能玩耍了,不需要再包含.cpp/.lib或者.dll之类的东西。当你的项目里有2个cpp文件[通常遇到问题是因为这两个cpp文件只有一个是你写的,另一个是你引用的其他第三方库里的],A.cpp include了rapidjson_v1.h,B.cpp include了 rapidjson_v2.h。这下,在编译阶段时候,编译器发现:"咦?怎么有两个class rapidjson定义,一个在A.cpp里,一个在B.cpp里。用哪一个呢"。其实这是C++普遍存在的问题,在.h里面定义了一个class或者template等东东,这个头文件被include到多个cpp里,在这些cpp里原样展开,编译器在链接的时候,就会看到多个重复的定义,于是C++规定了ODR(One Definition Rule),简而言之:"看到这种重复定义的类,且这些类的代码又长得一模一样,编译器就随便选一个用就行了"。因为量重复的这些定义都长得一样,就随便选一个都行了。这模式一直正常工作。再回到rapidjson,原本你想要的结果是A.cpp 使用rapidjson_v1.h里的class rapidjson,B.cpp 使用 rapidjson_v2.h里的class rapidjson。结果现在编译器不管是A.cpp还是B.cpp,都给你用rapidjson_v1.h里的class rapidjson[也有可能是rapidjson_v2.h里的class rapidjson]。编译器以为长一样,随便选一个就能正常工作,结果却不能正常工作,应该是rapidjson不同版本间做了一些违背ODR的变动。
PS:

Debug版本和Release版本其实就是优化级别的区别,Debug称为调试版本,编译的结果通常包含有调试信息,没有做任何优化,方便开发人员进行调试,Release称为发布版本,不会携带调试信息,同时编译器对代码进行了很多优化,使代码更小,速度更快,发布给用户使用,给用户使用以更好的体验。但Release模式编译比Debug模式花的时间也会更多。

原文链接:Effective C++笔记之二十一:One Definition Rule(ODR)-CSDN博客

相关推荐
ELI_He9993 分钟前
PHP中替换某个包或某个类
开发语言·php
小林熬夜学编程7 分钟前
【Linux网络编程】第十四弹---构建功能丰富的HTTP服务器:从状态码处理到服务函数扩展
linux·运维·服务器·c语言·网络·c++·http
m0_7482361110 分钟前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
倔强的石头10618 分钟前
【C++指南】类和对象(九):内部类
开发语言·c++
Watermelo61723 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
A懿轩A1 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
机器视觉知识推荐、就业指导2 小时前
C++设计模式:享元模式 (附文字处理系统中的字符对象案例)
c++
半盏茶香2 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
Evand J2 小时前
LOS/NLOS环境建模与三维TOA定位,MATLAB仿真程序,可自定义锚点数量和轨迹点长度
开发语言·matlab
LucianaiB2 小时前
探索CSDN博客数据:使用Python爬虫技术
开发语言·爬虫·python