Windows编译Linux CMake项目遇split模板函数重定义错误求助
在Windows 10系统下,使用MSYS2 64位和VSCode编译一个原本在Linux环境开发的CMake C++项目时,出现如下链接错误:
[build] [ 98%] Building CXX object CMakeFiles/heca_input_exe.dir/utils/options/structures_from_cmdline.cc.obj [build] [100%] Linking CXX executable heca_input_exe.exe [build] C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/12.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: CMakeFiles\heca_input_exe.dir/objects.a(string_utils.cc.obj): in function `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> > > >& utils::split<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, 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> > > >&, std::vector<char, std::allocator<char> > const&, bool, bool)': [build] C:/Users/pc/Documents/__protein design/___hydrogen bond/BioshellHydrogenBond/utils/string_utils.cc:62: multiple definition of `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> > > >& utils::split<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, 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> > > >&, std::vector<char, std::allocator<char> > const&, bool, bool)'; CMakeFiles\heca_input_exe.dir/objects.a(DataTable.cc.obj):C:/Users/pc/DOCUME~1/__protein design/___hydrogen bond/BioshellHydrogenBond/utils/string_utils.hh:140: first defined here
涉及的split()函数代码如下:
string_utils.hh 相关代码
//File: string_utils.hh // ... std::vector<std::string> split(const std::string &str, const std::vector<char> & delim = {' '}, const bool if_trim_tokens = true,const bool if_trim_line = true); void split(const std::string &input_str, const std::string &separator, const int max, std::vector<std::string> &results); // ... template<typename T> std::vector<T> & split(const std::string &s, std::vector<T> &tokens, const std::vector<char> & delim = {' '}, const bool if_trim_tokens = true,const bool if_trim_line = true) { std::string s_copy(s); trim(s_copy); for(int i=1;i<delim.size();++i) std::replace(s_copy.begin(), s_copy.end(), delim[i], delim[0]); std::stringstream ss(s_copy); std::string item; T data; while (std::getline(ss, item, delim[0])) { if (if_trim_tokens) trim(item); if (item.size() == 0) continue; if(item.find_first_not_of(' ') != std::string::npos) { std::stringstream ss2(item); ss2 >> data; tokens.push_back(data); } } return tokens; } // ...
string_utils.cc 相关代码
//File: string_utils.cc // ... void split(const std::string &input_str, const std::string &separator, const int max, std::vector<std::string> &results) { std::string str(input_str); int i = 0; size_t found = str.find_first_of(separator); while (found != std::string::npos) { if (found > 0) results.push_back(str.substr(0, found)); str = str.substr(found + 1); found = str.find_first_of(separator); if (max > -1 && ++i == max) break; } if (str.length() > 0) results.push_back(str); } template<> std::vector<std::string> &split<std::string>(const std::string &s, std::vector<std::string> &tokens, const std::vector<char> & delim, const bool if_trim_tokens,const bool if_trim_line) { std::string s_copy(s); for(int i=1;i<delim.size();++i) std::replace(s_copy.begin(), s_copy.end(), delim[i], delim[0]); if (if_trim_line==true) trim(s_copy); std::stringstream ss(s_copy); std::string item; while (std::getline(ss, item, delim[0])) { if (if_trim_tokens) trim(item); if (item.size() == 0) continue; tokens.push_back(item); } return tokens; } // ...
两个问题:
- 如何解决这个链接错误?
- 为何该错误在Linux环境下不会出现?
1. 解决链接错误的方法
这个错误的核心原因是:模板函数的隐式实例化和显式特化版本产生了重复定义。
当DataTable.cc包含string_utils.hh时,编译器会因为代码中使用了split<std::string>而自动隐式实例化模板函数,生成该函数的定义;而string_utils.cc中又存在split<std::string>的显式特化定义。链接时,两个目标文件(DataTable.cc.obj和string_utils.cc.obj)都包含了同一个函数的定义,导致冲突。
可以通过以下两种方案解决:
方案一:声明显式特化,阻止隐式实例化
在string_utils.hh的模板函数定义之后,添加split<std::string>的显式特化声明,告诉编译器不要自动生成该类型的实例,而是使用string_utils.cc中的特化版本:
// 在string_utils.hh的模板split<T>定义之后添加 template<> std::vector<std::string> &split<std::string>(const std::string &s, std::vector<std::string> &tokens, const std::vector<char> & delim, const bool if_trim_tokens,const bool if_trim_line);
这样其他编译单元包含头文件时,会知道该特化的存在,不会再隐式生成实例,链接时只会使用string_utils.cc中的定义,避免重复。
方案二:将显式特化移至头文件并标记为inline
把string_utils.cc中的显式特化代码移到string_utils.hh中,放在模板函数定义之后,并给特化函数加上inline关键字:
// 在string_utils.hh的模板split<T>定义之后添加 template<> inline std::vector<std::string> &split<std::string>(const std::string &s, std::vector<std::string> &tokens, const std::vector<char> & delim, const bool if_trim_tokens,const bool if_trim_line) { // 原特化函数的实现代码 }
inline关键字允许函数在多个编译单元中存在相同定义,链接器会自动合并这些定义,不会报错。
2. Linux环境下无错误的原因
主要是因为Linux和Windows的链接器对符号重复定义的处理规则不同:
- Linux使用ELF格式的目标文件,GCC链接器默认允许模板隐式实例化产生的弱符号与显式特化的强符号共存,会自动选择强符号(显式特化版本),不会报错。
- Windows下的MinGW链接器基于PE格式,对符号重复定义的检查更严格,当发现同一个符号存在两个不同的定义(隐式实例化和显式特化)时,直接抛出重复定义错误。
另外,Linux下的GCC可能默认启用了一些优化选项(如-fmerge-all-constants),进一步减少了这类冲突的概率,而MinGW的默认编译选项没有开启类似优化。
内容的提问来源于stack exchange,提问作者user366312

