[082]破局Cmake中的PRIVATE,PUBLIC,INTERFACE

前言

最近看了很多项目的代码,代码是用cmake编译的,由于各种库之间链接关系错综复杂,加上PRIVATE,PUBLIC,INTERFACE属性值,我在添加代码的时候总会遇到稀奇古怪的编译的问题,网上看了很多文章,写的都不是很靠谱,正好看到一个b站视频讲的不错,解决了我很多疑惑,我又有了新的疑惑,折腾了一晚上终于把这个搞明白了,分享给大家。

一、原理

从 modern cmake(>=3.0) 开始,使用的范式从 director-oriented 转换到了 target-oriented。 这其中最重要的有三个概念:

    target
    target相应的properties
    可见性

所谓target就是编译的目标,一般就三种:

    静态库: 使用add_library()
    动态库: 使用add_library() 指定SHARED关键字
    可执行文件: 使用add_executable

所谓properties就是target的属性,最常见的有以下五种:

    编译标志:使用target_complie_option
    预处理宏标志:使用 target_compile_definitions
    头文件目录:使用 target_include_directories
    链接库:使用 target_link_libraries
    链接标志:使用 target_link_options

所谓可见性就是上述这些属性在不同target之间的传递性。有三种:

    PRIVATE
    PUBLIC
    INTERFACE

    缺省值为PUBLIC

二、可见性的传递(非常重要)

每一个Target对于自身设置的不同属性处理

    对于private的property,不会传递,只会自己用。
    对于public的property,会传递,也自己用。
    对于interface的property,会传递,但不会自己用

    public和interface的属性是可传递属性

可见性的传递是依靠target_link_libraries,传递的规则如下:

假设如下链接关系
target_link_libraries(B XXX A)// XXX为private,public,interface

    如果XXX为private,A的可传递属性变成B的private property
    如果XXX为public,A的可传递属性变成B的public property
    如果XXX为interface,A的可传递属性变成B的interface property

三、实战1

3.1 最简单的demo


interface_a.h

#ifndef CPP_INTERFACE_A_H
#define CPP_INTERFACE_A_H
 
int addA(int a, int b);
 
#endif //CPP_INTERFACE_A_H

interface_b.h

#ifndef CPP_INTERFACE_B_H
#define CPP_INTERFACE_B_H
 
int addB(int a, int b);
#endif //CPP_INTERFACE_B_H

interface_a.cpp

#include <stdio.h>
#include "interface_a.h"
 
int addA(int a, int b) {
    printf("addA\n");
    return a + b;
}

interface_b.cpp

#include <stdio.h>
#include "interface_b.h"
#include "interface_a.h"
 
int addB(int a, int b)
{
    printf("addB\n");
    return addA(a, b);
}

main.cpp

#include "interface_b.h"
#include <stdio.h>
 
int main()
{
    printf("main\n");
    addB(1, 2);
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.22)
project(CPP)
 
set(CMAKE_CXX_STANDARD 17)
 
add_library(A libA/interface_a.c)
target_include_directories(A PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)
 
add_library(B SHARED libB/interface_b.c)
target_link_libraries(B A)
target_include_directories(B PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeB)
 
add_executable(CPP main.c)
target_link_libraries(CPP B)

用图来表示代码就如下,CPP调用B中addB,B中的addB调用addA



最后运行的结果

main
addB
addA

这例子简单吧,我们进一步来解读一下CMakeLists.txt,红色为传递过来的属性



查看对应的cmake的编译中间文件,可以进一步验证我们的判断,正好和对应的属性对应。


3.2 main中能否调用addA

可以看到CPP拥有target_include_directories(CPP PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA和target_link_libraries(CPP A)的属性
理论上来说肯定main.cpp可以调用addA

修改main.cpp

#include "interface_b.h"
#include "interface_a.h"
#include <stdio.h>
 
int main()
{
    printf("main\n");
    addA(1, 2);
    addB(1, 2);
    return 0;
}

成功运行

main
addA
addB
addA

3.3 将PUBLIC改成PRIVATE

如果我们对CMakeLists.txt做如下修改,请问上面main.c还能不能正常运行

cmake_minimum_required(VERSION 3.22)
project(CPP)
 
set(CMAKE_CXX_STANDARD 17)
 
add_library(A libA/interface_a.c)
target_include_directories(A PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)
 
add_library(B SHARED libB/interface_b.c)
target_link_libraries(B PRIVATE A)//改动的地方
target_include_directories(B PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeB)
 
add_executable(CPP main.c)
target_link_libraries(CPP B)

解读一下CmakeLists.txt,红色为传递过来的属性


和3.2中最大的差异就是CPP中includeA没了,那main.c肯定找不到#include "interface_a.h",所以会编译报错找不到头文件interface_a.h
运行结果果然和预料的一样。

/home/kobe/submits/CPP/main.c:2:10: fatal error: interface_a.h: No such file or directory
    2 | #include "interface_a.h"
      |          ^~~~~~~~~~~~~~~

3.4 手动添加includeA

继续修改 CMakeLists.txt

cmake_minimum_required(VERSION 3.22)
project(CPP)
 
set(CMAKE_CXX_STANDARD 17)
 
add_library(A libA/interface_a.c)
target_include_directories(A PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)
 
add_library(B SHARED libB/interface_b.c)
target_link_libraries(B PRIVATE A)
target_include_directories(B PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeB)
 
add_executable(CPP main.c)
target_link_libraries(CPP B)
target_include_directories(CPP PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)//修改的代码

解读一下CmakeLists.txt,红色为传递过来的属性,紫色是CPP额外加的属性


看到C自身属性添加了includeA,那头文件也有了,链接的时候,CPP链接B,B链接A,最后可以链接到一起,CPP应该可以使用addA了
运行结果果然可以

main
addA
addB
addA

四.实战2

4.1 Interface的作用

修改文件interface_b.cpp,移除B对A的addA的使用

#include <stdio.h>
#include "interface_b.h"
 
int addB(int a, int b)
{
    printf("addB\n");
    return a + b;
}

修改文件cmakelists.txt

cmake_minimum_required(VERSION 3.22)
project(CPP)
 
set(CMAKE_CXX_STANDARD 17)
 
add_library(A libA/interface_a.c)
target_include_directories(A PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)
 
add_library(B SHARED libB/interface_b.c)
target_link_libraries(B INTERFACE A)
target_include_directories(B PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeB)
 
add_executable(CPP main.c)
target_link_libraries(CPP B)

解读一下CmakeLists.txt,红色为传递过来的属性


因为CPP使用到A的接口和B的接口,B没有使用A的接口,所以按照上面的属性,A,B,CPP三个都可以正常编译运行

main
addA
addB

4.2 add_library(C INTERFACE) -- 比较特殊的用法

修改文件cmakelists.txt

cmake_minimum_required(VERSION 3.22)
project(CPP)
 
set(CMAKE_CXX_STANDARD 17)
 
add_library(A libA/interface_a.c)
target_include_directories(A PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)
 
add_library(C INTERFACE)
target_include_directories(C INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/includeB)
 
add_library(B SHARED libB/interface_b.c)
target_link_libraries(B PUBLIC C INTERFACE A)
 
add_executable(CPP main.c)
target_link_libraries(CPP B)

这是种特殊的用法,就是创建一个虚拟的target C,add_library(C INTERFACE)不会编译出任何库和可执行文件,而且C的所有属性必须设置为INTERFACE

解读一下CmakeLists.txt,红色为传递过来的属性



最后也可以完美的运行!

这里C就是一个header-only的库,他的所有属性都是Interface的,不会编译出任何库,唯一作用就是将属性传递给link它的目标。

五、总结

按照1.原理和2.可见性的传递,对应每一个项目,用这样子的表格列出来每一个target对应的属性,也就可以了解到每一个target编译依赖的头文件以及库文件。记住以Target的视角来看待每一个属性,关注两个Target之间的link的属性,以及两个Target之间的属性传递。

六、参考文献

https://chunleili.github.io/cmake/understanding-INTERFACE

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容