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

ODR细节有点复杂,跨越各种情况。基本内容如下:
●普通(非模板)的noninline函数和成员函数、noninline全局变量、静态数据成员在整个程序中都应当只定义一次。
●class类型(包括structs和unions)、模板(包括偏特化但是不包括全特化)、inline函数和inline变量在单个编译单元中最多定义一次,并且这些定义应该完全一样。
一个编译单元是源文件预处理后的结果,也就是说,它包含#include指令和宏拓展后的内容。与C语言一样,C++中所有的预处理指令都是以字符#开头,这些指令在编译之前进行处理。
本文将讨论一种违背ODR的典型情况:不同编译单元中包含同名结构体,结构体内函数定义相同,但数据成员不同
MyClass1.h

#ifndef MYCLASS1_H
#define MYCLASS1_Hclass MyClass1
{
public:MyClass1();
};#endif // MYCLASS1_H

MyClass1.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

#ifndef MYCLASS2_H
#define MYCLASS2_Hclass MyClass2
{
public:MyClass2();
};#endif // MYCLASS2_H

MyClass2.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

#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博客 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/869404.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2018-2022 年份微博签到数据集

前阵子接到一个实验室老师的需求&#xff0c;采集五年前&#xff08;2024-52019&#xff09;过年前后的北京微博签到数据。 前两年采集的深圳签到数据是 2022 年是当年的尚可&#xff0c;这次虽然时间跨度只有两个月&#xff0c;但是由于时间太过久远&#xff0c;但是颇费了一…

Spring学习04-[Spring容器核心技术AOP学习]

AOP学习 AOP介绍使用对业务方法添加计算时间的增强 EnableAspectJAutoProxyAOP的术语通知前置通知Before后置通知After返回通知AfterReturning异常通知AfterThrowing总结-通知执行顺序 切点表达式的提取-使用Pointcut进行抽取切点表达式的详细用法execution和annotation组合 Sp…

STM32快速搭建项目框架

注&#xff1a;编写本博客的原因&#xff0c;学习期间基于复习之前知识点的需要&#xff0c;故撰写本教程&#xff0c;即是复习前面的知识点也是作为博客的补充 1.0 文件夹的创建 创建一个STM32项目为模版工程&#xff0c;问价夹下分别包含4个子文件夹&#xff0c;一个是Librar…

嘉立创EDA学习笔记

嘉立创EDA学习笔记 PCB引线一、设计规则间距安全间距其他间距 物理导线网络长度差分对过孔尺寸 平面铺铜 PCB布线 作为一个嵌入式开发潜力工程师&#xff0c;咱们必须得学会如何绘制开发板以满足顾客各种功能的需求&#xff0c;因此小编去学习了一下嘉立创&#xff0c;写这篇文…

VSCode用ssh连接ubuntu虚拟机实现远程访问文件夹

1. ubuntu安装ssh服务 1.1 安装 sudo apt-get install ssh sudo apt-get install openssh-server1.2 启动ssh服务 sudo service ssh start sudo service ssh status # 查看状态 ## 或者用下面方式重启ssh服务 ## /etc/init.d/ssh restart1.3 ssh服务加入开机启动 sudo syst…

HTML语言常见标签

语法 HEAD部分的HTML标签 1 标题标签 <title>标题内容</title> 2 段落标签 <meta charset"utf-8"/> BODY部分的HTML标签 1标题标签&#xff08;独占一行&#xff09;<h1>标题内容</h1> 2段落标签&#xff08;独占一行&#xff09;…

TK 检查输入框是否为空

在Python的Tkinter库中&#xff0c;你可以使用事件绑定或者在按钮点击事件中检查输入框的值是否为空来实现这个功能。以下是一个简单的例子&#xff1a; import tkinter as tk from tkinter import messageboxdef check_input():entry input_box.get()if not entry:messagebo…

TLP152 光耦合器:工程师的可靠选择

东芝的 TLP152 光耦合器是一款稳健且多功能的组件&#xff0c;能够满足各种高速和高可靠性应用中的工程师需求。本文将深入探讨 TLP152 的技术特性、优点和应用&#xff0c;突出其在市场中的独特性。 主要特点和规格 TLP152 光耦合器集成了一颗铝镓砷&#xff08;GaAlAs&…

昇思14天

ResNet50图像分类 1. ResNet50图像分类概述 ResNet50是一种用于图像分类的深度卷积神经网络。图像分类是计算机视觉的基本应用&#xff0c;属于有监督学习范畴。ResNet50通过引入残差结构&#xff0c;解决了深层网络中的退化问题&#xff0c;使得可以训练非常深的网络。 2. …

了解Adam和RMSprop优化算法

优化算法是机器学习和深度学习模型训练中至关重要的部分。本文将详细介绍Adam&#xff08;Adaptive Moment Estimation&#xff09;和RMSprop&#xff08;Root Mean Square Propagation&#xff09;这两种常用的优化算法&#xff0c;包括它们的原理、公式和具体代码示例。 RMS…

配置路由器支持Telnet操作 计网实验

实验要求&#xff1a; 假设某学校的网络管理员第一次在设备机房对路由器进行了初次配置后&#xff0c;他希望以后在办公室或出差时也可以对设备进行远程管理&#xff0c;现要在路由器上做适当配置&#xff0c;使他可以实现这一愿望。 本实验以一台R2624路由器为例&#xff0c;…

OpenCV MEI相机模型(全向模型)

文章目录 一、简介二、实现代码三、实现效果参考文献一、简介 对于针孔相机模型,由于硬件上的限制(如进光量等),他的视野夹角往往有效区域只有140度左右,因此就有研究人员为每个针孔相机前面再添加一个镜片,如下所示: 通过折射的方式增加了相机成像的视野,虽然仍然达不…

东方通Tongweb发布vue前端

一、前端包中添加文件 1、解压vue打包文件 以dist.zip为例&#xff0c;解压之后得到dist文件夹&#xff0c;进入dist文件夹&#xff0c;新建WEB-INF文件夹&#xff0c;进入WEB-INF文件夹&#xff0c;新建web.xml文件&#xff0c; 打开web.xml文件&#xff0c;输入以下内容 …

理解局域网技术:从基础到进阶

局域网&#xff08;LAN&#xff09;是在20世纪70年代末发展起来的&#xff0c;起初主要用于连接单位内部的计算机&#xff0c;使它们能够方便地共享各种硬件、软件和数据资源。局域网的主要特点是网络为一个单位所拥有&#xff0c;地理范围和站点数目均有限。 局域网技术在计算…

RequestContextHolder多线程获取不到request对象

RequestContextHolder多线程获取不到request对象&#xff0c;调用feign接口时&#xff0c;在Feign中的RequestInterceptor也获取不到HttpServletRequest问题解决方案。 1.RequestContextHolder多线程获取不到request对象 异常信息&#xff0c;报错如下&#xff1a; 2024-07-0…

(四)前端javascript中的数据结构之归并排序

归并排序是一种分治算法&#xff0c; 其思想是&#xff1a; 将原始数组切分成较小的数组&#xff0c;直到每个小数组只有一 个位置&#xff0c;接着将小数组归并成较大的数组&#xff0c;直到最后只有一个排序完毕的大数组 归并排序是第一个可以被实际使用的排序算法。它比前面…

SpringBoot实现简单AI问答(百度千帆)

第一步&#xff1a;注册并登录百度智能云&#xff0c;创建应用并获取自己的APIKey与SecretKey&#xff0c;参考网址&#xff1a; 点击去百度智能云 第二步&#xff1a;引入千帆的pom依赖 <dependency><groupId>com.baidubce</groupId><artifactId>q…

Jenkins 构建 Web 项目:构建服务器和部署服务器分离, 并且前后端在一起的项目

构建命令 #!/bin/bash cd ruoyi-ui node -v pnpm -v pnpm install pnpm build:prod # 将dist打包成dist.zip zip -r dist.zip dist cp dist.zip ../dist.zip

【Linux】动态库的制作与使用

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

Linux——多线程(四)

前言 这是之前基于阻塞队列的生产消费模型中Enqueue的代码 void Enqueue(const T &in) // 生产者用的接口{pthread_mutex_lock(&_mutex);while(IsFull())//判断队列是否已经满了{pthread_cond_wait(&_product_cond, &_mutex); //满的时候就在此情况下等待// 1.…