c++ 声明定义都在头文件中怎么include?

下了一些百度webapi代码,定义声明都在.h文件中,没有.cpp文件。
我的代码是.h .cpp分开的,当我包含那些.h后,编译就报错一堆重复定义
下载连接地址:百度在线语音识别下载链接
图片描述

——————————————————————————————
好像图片显示不了,粘贴一段异常

g++ main.cpp RecogBaiduOnline.cpp -L. -ljsoncpp -lcrypto -lcurl -g -o main -std=c++11
/tmp/ccVleVa4.o:在函数‘void std::_Destroy_aux<false>::__destroy<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*)’中:
/home/dyan/projects/recognition_sound/base/base64.h:35: `aip::base64_encode[abi:cxx11](char const*, unsigned int)'被多次定义
/tmp/cchpsjly.o:/home/dyan/projects/recognition_sound/base/base64.h:35:第一次在此定义
/tmp/ccVleVa4.o:在函数‘void std::__adjust_heap<__gnu_cxx::__normal_iterator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, long, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, __gnu_cxx::__ops::_Iter_less_iter>(__gnu_cxx::__normal_iterator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, long, long, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, __gnu_cxx::__ops::_Iter_less_iter)’中:
/home/dyan/projects/recognition_sound/base/base64.h:88: `aip::base64_decode(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'被多次定义
/tmp/cchpsjly.o:/home/dyan/projects/recognition_sound/base/base64.h:88:第一次在此定义
/tmp/ccVleVa4.o:在函数‘aip::utc_time[abi:cxx11](long)’中:
/home/dyan/projects/recognition_sound/base/utils.h:79: `aip::utc_time[abi:cxx11](long)'被多次定义
/tmp/cchpsjly.o:/home/dyan/projects/recognition_sound/base/utils.h:79:第一次在此定义

—————————————————————————
把源码中重复定义的函数都加了static inline,只保留了第一个重复定义函数没有加inline用来显示错误,加了g++ -v参数,完整编译信息

g++ main.cpp RecogBaiduOnline.cpp -L. -ljsoncpp -lcrypto -lcurl -g -v -o main -std=c++11
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.9' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9) 
COLLECT_GCC_OPTIONS='-L.' '-g' '-v' '-o' 'main' '-std=c++11' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/5/cc1plus -quiet -v -imultiarch x86_64-linux-gnu -D_GNU_SOURCE main.cpp -quiet -dumpbase main.cpp -mtune=generic -march=x86-64 -auxbase main -g -std=c++11 -version -fstack-protector-strong -Wformat -Wformat-security -o /tmp/ccQmpXV5.s
GNU C++11 (Ubuntu 5.4.0-6ubuntu1~16.04.9) version 5.4.0 20160609 (x86_64-linux-gnu)
    compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring duplicate directory "/usr/include/x86_64-linux-gnu/c++/5"
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/5/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/5
 /usr/include/x86_64-linux-gnu/c++/5
 /usr/include/c++/5/backward
 /usr/lib/gcc/x86_64-linux-gnu/5/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
GNU C++11 (Ubuntu 5.4.0-6ubuntu1~16.04.9) version 5.4.0 20160609 (x86_64-linux-gnu)
    compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: d8217bc73be730fa402b500d5726a5b4
COLLECT_GCC_OPTIONS='-L.' '-g' '-v' '-o' 'main' '-std=c++11' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
 as -v --64 -o /tmp/ccPnErzo.o /tmp/ccQmpXV5.s
GNU汇编版本 2.26.1 (x86_64-linux-gnu) 使用BFD版本 (GNU Binutils for Ubuntu) 2.26.1
COLLECT_GCC_OPTIONS='-L.' '-g' '-v' '-o' 'main' '-std=c++11' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/5/cc1plus -quiet -v -imultiarch x86_64-linux-gnu -D_GNU_SOURCE RecogBaiduOnline.cpp -quiet -dumpbase RecogBaiduOnline.cpp -mtune=generic -march=x86-64 -auxbase RecogBaiduOnline -g -std=c++11 -version -fstack-protector-strong -Wformat -Wformat-security -o /tmp/ccQmpXV5.s
GNU C++11 (Ubuntu 5.4.0-6ubuntu1~16.04.9) version 5.4.0 20160609 (x86_64-linux-gnu)
    compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring duplicate directory "/usr/include/x86_64-linux-gnu/c++/5"
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/5/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/5
 /usr/include/x86_64-linux-gnu/c++/5
 /usr/include/c++/5/backward
 /usr/lib/gcc/x86_64-linux-gnu/5/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
GNU C++11 (Ubuntu 5.4.0-6ubuntu1~16.04.9) version 5.4.0 20160609 (x86_64-linux-gnu)
    compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: d8217bc73be730fa402b500d5726a5b4
COLLECT_GCC_OPTIONS='-L.' '-g' '-v' '-o' 'main' '-std=c++11' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
 as -v --64 -o /tmp/ccCqvBxL.o /tmp/ccQmpXV5.s
GNU汇编版本 2.26.1 (x86_64-linux-gnu) 使用BFD版本 (GNU Binutils for Ubuntu) 2.26.1
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-L.' '-g' '-v' '-o' 'main' '-std=c++11' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt=-fresolution=/tmp/ccPnBSQ8.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro -o main /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o -L. -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. /tmp/ccPnErzo.o /tmp/ccCqvBxL.o -ljsoncpp -lcrypto -lcurl -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o
/tmp/ccCqvBxL.o:在函数‘__gnu_cxx::new_allocator<ASR_result*>::new_allocator()’中:
/home/dyan/projects/recognition_sound/base/base64.h:35: `aip::base64_encode[abi:cxx11](char const*, unsigned int)'被多次定义
/tmp/ccPnErzo.o:/home/dyan/projects/recognition_sound/base/base64.h:35:第一次在此定义
collect2: error: ld returned 1 exit status
makefile:9: recipe for target 'main' failed
make: *** [main] Error 1

——————————————————————————————
然后看了下代码,有预编译,不是重复包含的问题。只看前2个报的重复定义35行和88行
图片描述

之后我也把自己的声明和定义都写到.h文件中并include百度的.h,没有了.cpp文件,异常就消失了。
问题:
1.这样做就让我之后include的.h文件中只要有include百度的.h就全部要做成这样吗?这很不现实啊。
2.这种异常是如何产生的?
3.如何解决?

阅读 5.5k
3 个回答

使用你提供的 SDK 下载链接,亲测可编译(linux x64 g++)。

源码目录结构如下

.
├── a.cpp
├── a.h
├── aip-cpp-sdk-0.4.0
│   ├── base
│   │   ├── base64.h
│   │   ├── base.h
│   │   ├── http.h
│   │   └── utils.h
│   ├── face.h
│   ├── image_censor.h
│   ├── image_classify.h
│   ├── image_search.h
│   ├── kg.h
│   ├── nlp.h
│   ├── ocr.h
│   ├── README.md
│   └── speech.h
├── main.cpp
└── Makefile

文件 a.h 内容如下

#ifndef _a_h_
#define _a_h_
bool call_speech();
#endif

文件 a.cpp 内容如下

#include "aip-cpp-sdk-0.4.0/speech.h"
#include <json/json.h>

bool call_speech() {
    aip::Speech sp("app_id", "ak", "sk");
    Json::Value data;
    Json::Value result = sp.request_asr("url", data);
    return result.isString();
}

文件 main.cpp 内容如下

#include "a.h"

int main()
{
    return call_speech() ? 0: 1;
}

文件 Makefile 内容如下

demo: a.cpp main.cpp
    g++ -Wall -std=c++11 -g $^ -o $@ -lcurl -lcrypto -ljsoncpp

使用命令 make 顺利编译。


由于你没有贴出调用 SDK 的相关代码,我无法准确判断原因。

以上面的代码结构为例,你很可能在 a.h 里引用了 SDK 的头文件,然后在 a.cpp 里面引用了 a.h 头文件,类似这样

// a.h 文件内容
#ifndef _a_h_
#define _a_h_
#include "aip-cpp-sdk-0.4.0/speech.h"
#include <json/json.h>

bool call_speech();
#endif


// a.cpp 文件内容
#include "a.h"
#include <json/json.h>

bool call_speech() {
.......

这种写法就会出现 “重复定义” 的错误,。


SDK 下载地址: http://ai.baidu.com/download?...

看起来最可能是因为这个头文件不是用来让你包含的。

如何产生的?
这个链接错误是由于违背了ODR。即在你的程序中,同一个非内联函数被多次定义。

解决方案:

  1. 不要包含这样的头文件。
  2. 在所有这样的函数前加上inline。
  3. 只在一个源文件中包含这样的头文件。

当然还有很多别的方案,就不一一列举了。总之,你需要保证在所有参与编译的源文件(更准确的说是参与链接的目标文件)中,这样的头文件(定义了非内联函数的头文件)至多只被其中的一个包含。

PS:和命名空间完全没关系。

瀉藥, @李毅 老大已經點名你出錯的地方了, @felix 老大也指出是ODR的問題, 看來窩除了能在上面說下原理沒什麼做了, 哈哈. 不過既然兩位老大都沒有將原理和你的庫結合, 那麼這個微小工作就由窩踩在兩位老大的肩膀上來完成吧.

科普基礎知識

窩給你結合標準文檔從頭梳理一些c++裏面必須知道的入門常識性概念, 這些概念可能你會覺得過有點多餘, 但是這些是每一個寫c++的必須知道的基礎. 並且窩在這裏會剔除些標準裏的wording, 只會提及此處需要用到的概念.

以下引用皆出自N4741, 歌詞大意可以理解爲Informally的簡略版解釋(可能有錯誤的私貨, 如有老大看出, 望告知)

Include guard

這在標準裏面沒有, 只是c++程序員爲了實現ODR的一個慣用法罷了, 通過conditional inclusion在頭文件裏面定義大部分時間都可以用#pragma once 來代替, 不過窩有一次被一個大佬警告過其可移植性, 但是窩查了下發現msvc, clang, gcc, icc, xl都zici呀...可能是有標準潔癖吧.

Translation unit

The text of the program is kept in units called source files in this document. A source file together with all the headers (20.5.1.2) and source files included (19.2) via the preprocessing directive#include, less any source lines skipped by any of the conditional inclusion (19.1) preprocessing directives, is called a translation unit.

歌詞大意: 每一個源文件(.cpp/.cc/.cxx等)在使用了include guard後展開頭文件(.hh/.hpp/.h等)(即複製頭文件所有內容進源文件)

Declarations and definitions

Each entity declared by a declaration is also defined by that declaration, unless..........

歌詞大意, $$\mathsf{definitions} \subset \mathsf{declarations}$$, 這就是這兩者的關係, 不過窩在某個dlang的群裏發現很多寫了c++多年的選手依然不知道這一點, 這是很致命的, 比如不熟悉其區別可能就會產生窩這樣的困惑:https://stackoverflow.com/que... 還可能有其它危害, 下文也會提及.

One Definition Rule(ODR)

在n4741中, ODR分爲了12大點, 以後還可能擴充或修改, 前些日子在so上看到一個語言律師在odr-use上發現了自相矛盾的地方. 不過這裏只需要說下兩點就行了.

1) No translation unit shall contain more than one definition of any variable, function, class type, enumeration type, or template.

ODR-use

本來不想說odr-used的, 但是發現還是脫不開它.

A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless applying the lvalue-to-rvalue conversion (7.1) to x yields a constant expression (8.6) that does not invoke any non-trivial functions and, if x is an object,ex is an element of the set of potential results of an expressione, where either the lvalue-to-rvalue conversion (7.1) is applied toe, or e is a discarded-value expression (8.2).

歌詞大意: 先要瞭解potentially-evaluated expression是什麼, 花個1分鐘看下這個帖子: https://stackoverflow.com/que... 好, 當ex滿足potentially-evaluated expression的性質時, 除非做了左值->右值的轉換(如x作爲返回值, 但是是按值返回的), 亦或是x本身不是object, 比如x可以是引用(另一種情況這不解釋了, 不然牽扯的概念就太多了). 好, 說這些可能會有點混, 其實, 你需要知道的是odr-use的意思就是如字面所說--需要definition的存在, 而不僅僅是declaration(現在你應該明白爲什麼窩在一開始就要區分definition和declaration及其子集關係了吧).

繼續回到One definition rule的定義

10) Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program outside of a discarded statement (9.4.1); no diagnostic required. The definition can appear explicitly in the program, it can be found in the standard or a user-defined library, or (when appropriate) it is implicitly defined (see 15.1, 15.4 and 15.8). An inline function or variable shall be defined in every translation unit in which it is odr-used outside of a discarded statement.

歌詞大意: 對於函數或者變量, 在整個程序中也要遵循odr原則, 但是很明顯頭文件會被很多源文件包含, 該怎麼辦呢? inline用來開洞解決這個問題, 加了inline, 對函數的odr檢查就被無視了(當然, 很多時候會自動inline, 比如類內定義, friend等).


根據原理剖析錯因.

窩不知道你自己的程式結構, 那麼我就沿用@李毅 老大給出的文件組織了:

├── a.cpp
├── a.h
├── aip-cpp-sdk-0.4.0
│   ├── base
│   │   ├── base64.h
│   │   ├── base.h
│   │   ├── http.h
│   │   └── utils.h
│   ├── face.h
│   ├── image_censor.h
│   ├── image_classify.h
│   ├── image_search.h
│   ├── kg.h
│   ├── nlp.h
│   ├── ocr.h
│   ├── README.md
│   └── speech.h
├── main.cpp
└── Makefile

很明顯, translation parsion之後我們由a.cppmain.cpp兩個翻譯單元存在, 好, 我們通過展開這兩個翻譯單元來分析爲什麼你自己寫的(即版本二)會違背ODR, 而@李毅 老大給出的第一個版本就不會. 爲了進一步簡化問題, 我們把#include <json/json.h>不予考慮, 規定base/base.h的內容除去include guard僅有#include "base64.h一行, base/base64.h除去include guard

include "iostream" 
namespace aip { void f() { std::cout << "hello"; }

speech.h除去include guard

#include "base/base.h"
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

main.c有:

#include "a.h"
int main()
{
    f_a();
}

版本一

a.cpp(稍作簡化)
#include "aip-cpp-sdk-0.4.0/speech.h"

void f_a()
{
    aip::f_s();
}

$$=>$$

#include "aip-cpp-sdk-0.4.0/base/base.h"
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

void f_a()
{
    aip::f_s();
}

$$=>$$

#include "aip-cpp-sdk-0.4.0/base/base64.h"
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

void f_a()
{
    aip::f_s();
}

$$=>$$

include <iostream>
namespace aip { void f() { std::cout << "hello"; }
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

void f_a()
{
    aip::f_s();
}
main.cpp
#include "a.h"

int main()
{
    f_a();
}

$$=>$$

bool f_a();
int main()
{
    f_a();
}
小結

可見這個版本展開到最後沒有違背ODR的部分.

版本二

a.cpp
#include "a.h"

void f_a()
{
    aip::f_s();
}

$$=>$$

#include "aip-cpp-sdk-0.4.0/base/base64.h"
void f_a();

void f_a()
{
    aip::f_s();
}

接下來的步驟幾乎於版本一相同了, 直接貼最後結果:


void f_a();

include <iostream>
namespace aip { void f() { std::cout << "hello"; }
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

void f_a()
{
    aip::f_s();
}
main.cpp
#include "a.h"
int main()
{
    f_a();
}

只要展開a.h, 上面也有此步驟, 所以直接給結果:

void f_a();

include <iostream>
namespace aip { void f() { std::cout << "hello"; }
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

int main()
{
    f_a();
}

小結

好, 其實我們很容易發現, 版本二的兩個翻譯單元都會包含aip::f_i()aip::f_i的definition, 再結合原理部分的最後一段, 這明顯違反了ODR`, 所以是錯誤的

解決方案

兩位老大已經給出了方案了, 要麼你自己的頭文件和源文件小心處理, 理清依賴關係, 要麼你就給庫函數加上inline, 屏蔽ODR檢查.

誰背鍋?

  1. c++. 沒有module, 只能依靠這種落伍的include guard和inline來解決這種包含和符號檢查, 很容易會混亂心智
  2. c. 歷史殘留
  3. 庫的作者, 窩懷疑他們自己都沒有用過自己寫的這個庫.

@藤壶女御_ 老大誤以爲窩是臺灣人, 真是夭壽啦:P 畢竟窩吃不起茶葉蛋, 用着799碎屏紅米(碎1年了), 穿着迪卡農的鞋子, 筆記本的價格也只有1700RMB(3年了已經), 只是最近喜歡上繁體字了, 所以窩用byvoid老大的Open CC提供簡體版:



泻药, @李毅 老大已经点名你出错的地方了, @felix 老大也指出是ODR的问题, 看来窝除了能在上面说下原理没什么做了, 哈哈. 不过既然两位老大都没有将原理和你的库结合, 那么这个微小工作就由窝踩在两位老大的肩膀上来完成吧.

科普基础知识

窝给你结合标准文档从头梳理一些c++里面必须知道的入门常识性概念, 这些概念可能你会觉得过有点多余, 但是这些是每一个写c++的必须知道的基础. 并且窝在这里会剔除些标准里的wording, 只会提及此处需要用到的概念.

以下引用皆出自N4741, 歌词大意可以理解为Informally的简略版解释(可能有错误的私货, 如有老大看出, 望告知)

Include guard

这在标准里面没有, 只是c++程序员为了实现ODR的一个惯用法罢了, 通过conditional inclusion在头文件里面定义大部分时间都可以用#pragma once 来代替, 不过窝有一次被一个大佬警告过其可移植性, 但是窝查了下发现msvc, clang, gcc, icc, xl都zici呀...可能是有标准洁癖吧.

Translation unit

The text of the program is kept in units called source files in this document. A source file together with all the headers (20.5.1.2) and source files included (19.2) via the preprocessing directive#include, less any source lines skipped by any of the conditional inclusion (19.1) preprocessing directives, is called a translation unit.

歌词大意: 每一个源文件(.cpp/.cc/.cxx等)在使用了include guard后展开头文件(.hh/.hpp/.h等)(即复制头文件所有内容进源文件)

Declarations and definitions

Each entity declared by a declaration is also defined by that declaration, unless..........

歌词大意, $$\mathsf{definitions} \subset \mathsf{declarations}$$, 这就是这两者的关系, 不过窝在某个dlang的群里发现很多写了c++多年的选手依然不知道这一点, 这是很致命的, 比如不熟悉其区别可能就会产生窝这样的困惑:https://stackoverflow.com/que... 还可能有其它危害, 下文也会提及.

One Definition Rule(ODR)

在n4741中, ODR分为了12大点, 以后还可能扩充或修改, 前些日子在so上看到一个语言律师在odr-use上发现了自相矛盾的地方. 不过这里只需要说下两点就行了.

1) No translation unit shall contain more than one definition of any variable, function, class type, enumeration type, or template.

ODR-use

本来不想说odr-used的, 但是发现还是脱不开它.

A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless applying the lvalue-to-rvalue conversion (7.1) to x yields a constant expression (8.6) that does not invoke any non-trivial functions and, if x is an object,ex is an element of the set of potential results of an expressione, where either the lvalue-to-rvalue conversion (7.1) is applied toe, or e is a discarded-value expression (8.2).

歌词大意: 先要了解potentially-evaluated expression是什么, 花个1分钟看下这个帖子: https://stackoverflow.com/que... 好, 当ex满足potentially-evaluated expression的性质时, 除非做了左值->右值的转换(如x作为返回值, 但是是按值返回的), 亦或是x本身不是object, 比如x可以是引用(另一种情况这不解释了, 不然牵扯的概念就太多了). 好, 说这些可能会有点混, 其实, 你需要知道的是odr-use的意思就是如字面所说--需要definition的存在, 而不仅仅是declaration(现在你应该明白为什么窝在一开始就要区分definition和declaration及其子集关系了吧).

继续回到One definition rule的定义

10) Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program outside of a discarded statement (9.4.1); no diagnostic required. The definition can appear explicitly in the program, it can be found in the standard or a user-defined library, or (when appropriate) it is implicitly defined (see 15.1, 15.4 and 15.8). An inline function or variable shall be defined in every translation unit in which it is odr-used outside of a discarded statement.

歌词大意: 对于函数或者变量, 在整个程序中也要遵循odr原则, 但是很明显头文件会被很多源文件包含, 该怎么办呢? inline用来开洞解决这个问题, 加了inline, 对函数的odr检查就被无视了(当然, 很多时候会自动inline, 比如类内定义, friend等).


根据原理剖析错因.

窝不知道你自己的程式结构, 那么我就沿用@李毅 老大给出的文件组织了:

├── a.cpp
├── a.h
├── aip-cpp-sdk-0.4.0
│   ├── base
│   │   ├── base64.h
│   │   ├── base.h
│   │   ├── http.h
│   │   └── utils.h
│   ├── face.h
│   ├── image_censor.h
│   ├── image_classify.h
│   ├── image_search.h
│   ├── kg.h
│   ├── nlp.h
│   ├── ocr.h
│   ├── README.md
│   └── speech.h
├── main.cpp
└── Makefile

很明显, translation parsion之后我们由a.cppmain.cpp两个翻译单元存在, 好, 我们通过展开这两个翻译单元来分析为什么你自己写的(即版本二)会违背ODR, 而@李毅 老大给出的第一个版本就不会. 为了进一步简化问题, 我们把#include <json/json.h>不予考虑, 规定base/base.h的内容除去include guard仅有#include "base64.h一行, base/base64.h除去include guard

include "iostream" 
namespace aip { void f() { std::cout << "hello"; }

speech.h除去include guard

#include "base/base.h"
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

main.c有:

#include "a.h"
int main()
{
    f_a();
}

版本一

a.cpp(稍作简化)
#include "aip-cpp-sdk-0.4.0/speech.h"

void f_a()
{
    aip::f_s();
}

$$=>$$

#include "aip-cpp-sdk-0.4.0/base/base.h"
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

void f_a()
{
    aip::f_s();
}

$$=>$$

#include "aip-cpp-sdk-0.4.0/base/base64.h"
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

void f_a()
{
    aip::f_s();
}

$$=>$$

include <iostream>
namespace aip { void f() { std::cout << "hello"; }
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

void f_a()
{
    aip::f_s();
}
main.cpp
#include "a.h"

int main()
{
    f_a();
}

$$=>$$

bool f_a();
int main()
{
    f_a();
}
小结

可见这个版本展开到最后没有违背ODR的部分.

版本二

a.cpp
#include "a.h"

void f_a()
{
    aip::f_s();
}

$$=>$$

#include "aip-cpp-sdk-0.4.0/base/base64.h"
void f_a();

void f_a()
{
    aip::f_s();
}

接下来的步骤几乎于版本一相同了, 直接贴最后结果:


void f_a();

include <iostream>
namespace aip { void f() { std::cout << "hello"; }
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

void f_a()
{
    aip::f_s();
}
main.cpp
#include "a.h"
int main()
{
    f_a();
}

只要展开a.h, 上面也有此步骤, 所以直接给结果:

void f_a();

include <iostream>
namespace aip { void f() { std::cout << "hello"; }
namespace aip {
    void f_s() {
        aip::f_i();
    }
}

int main()
{
    f_a();
}

小结

好, 其实我们很容易发现, 版本二的两个翻译单元都会包含aip::f_i()aip::f_i的definition, 再结合原理部分的最后一段, 这明显违反了ODR`, 所以是错误的

解决方案

两位老大已经给出了方案了, 要么你自己的头文件和源文件小心处理, 理清依赖关系, 要么你就给库函数加上inline, 屏蔽ODR检查.

谁背锅?

  1. c++. 没有module, 只能依靠这种落伍的include guard和inline来解决这种包含和符号检查, 很容易会混乱心智
  2. c. 历史残留
  3. 库的作者, 窝怀疑他们自己都没有用过自己写的这个库.

@藤壶女御_ 老大误以为窝是台湾人, 真是夭寿啦:P 毕竟窝吃不起茶叶蛋, 用着799碎屏红米(碎1年了), 穿着迪卡农的鞋子, 笔记本的价格也只有1700RMB(3年了已经), 只是最近喜欢上繁体字了

參考: http://eel.is/c++draft/#tab

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
1 篇内容引用
推荐问题