使用typedef定义带自定义比较器的set跨文件调用时出现编译链接错误
问题
尝试用typedef定义带自定义比较器的std::set,跨文件使用时出现编译链接错误。简化后的代码如下:
A.hpp
#ifndef A_HEADER #define A_HEADER #include <set> struct Tuple { int i; char c; }; auto cmp = [](const Tuple &a, const Tuple &b) { return a.i < b.i; }; typedef std::set<Tuple, decltype(cmp)> TupleSet; TupleSet getTuples(void); #endif
A.cpp
#include "A.hpp" TupleSet getTuples(void) { TupleSet s(cmp); for (int i = 0; i < 5; ++i) { Tuple t = {i, 'a'}; s.insert(t); } return s; }
B.hpp
#ifndef B_HEADER #define B_HEADER #include "A.hpp" void foo(void); #endif
B.cpp
#include "B.hpp" #include <iostream> void foo(void) { TupleSet s = getTuples(); for (const Tuple &t : s) { std::cout << "(" << t.i << ", " << t.c << ")\n"; } } int main(void) { foo(); }
编译命令:
clang++ -c -o A.o A.cpp clang++ -o B B.cpp A.o
出现的错误:
In file included from B.cpp:1:
In file included from ./B.hpp:4:
./A.hpp:15:10: warning: function 'getTuples' has internal linkage but is not defined [-Wundefined-internal]
TupleSet getTuples(void);
^
B.cpp:5:18: note: used here
TupleSet s = getTuples();
^
1 warning generated.
/usr/bin/ld: /tmp/B-ec93ec.o: in functionfoo()': B.cpp:(.text+0x10): undefined reference togetTuples()'
clang: error: linker command failed with exit code 1 (use -v to see invocation)
问题原因与解决方法
核心原因
头文件A.hpp中,auto cmp = []{...};定义的lambda是内部链接变量——头文件被多个cpp包含时,每个编译单元都会生成独立的cmp实例。而每个lambda的类型都是唯一的,哪怕代码完全一致,这就导致decltype(cmp)在A.cpp和B.cpp中被推导为不同类型,最终让两个文件中getTuples的返回类型不匹配,链接器找不到对应实现。
解决方法
有三种可行的修复方式:
方式一:用函数对象类替代lambda
把比较逻辑封装成具名类,确保所有编译单元中类型一致:
// 修改A.hpp中的cmp和typedef struct TupleCmp { bool operator()(const Tuple &a, const Tuple &b) const { return a.i < b.i; } }; typedef std::set<Tuple, TupleCmp> TupleSet;
此时A.cpp中初始化set无需传参数,直接写TupleSet s;即可。
方式二:让lambda拥有外部链接
C++17及以上版本可将cmp声明为inline,保证所有编译单元共享同一个lambda实例,类型统一:
// 修改A.hpp中的cmp定义 inline auto cmp = [](const Tuple &a, const Tuple &b) { return a.i < b.i; };
这种方式保留lambda写法,同时解决类型不一致问题。
方式三:使用函数指针作为比较器
用普通函数替代lambda,以函数指针类型作为set的比较器:
// 修改A.hpp bool cmp(const Tuple &a, const Tuple &b) { return a.i < b.i; } typedef std::set<Tuple, bool(*)(const Tuple&, const Tuple&)> TupleSet;
A.cpp中初始化set时需传入函数指针:TupleSet s(&cmp);,但这种方式性能略逊于函数对象(函数指针无法被内联)。
内容的提问来源于stack exchange,提问作者terrabyte

