Clang与GCC因重载模板add函数编译卡顿问题咨询
问题背景
我定义了抽象基类Base,以及继承自它的Derived1和Derived2类;还有Container类,其内部用std::map存储指向Base对象的std::unique_ptr以实现运行时多态。Container类有两个重载的add函数:
- 一个接收
std::unique_ptr<Base>类型参数并添加到map中; - 另一个是模板函数,接收任意类型T的对象,将其移入
std::unique_ptr<T>后调用前一个add函数。
最小复现代码
#include <map> #include <memory> #include <string> class Base { public: virtual ~Base(); }; class Derived1 : Base { }; class Derived2 : Base { }; class Container { public: std::map<std::string, std::unique_ptr<Base>> coll; void add(std::string name, std::unique_ptr<Base> elem) { coll.emplace(name, std::move(elem)); } template<typename T> void add(std::string name, T elem) { this->add(name, std::make_unique<T>(std::move(elem))); } }; int main() { Container c; //比c.add("1", std::make_unique<Derived1>());更简便 c.add("1", Derived1()); }
编译时遇到的问题:
- 使用Clang 16编译时,编译器会卡顿并持续占用内存直至耗尽32GB内存;
- GCC也会卡顿,但不会持续占用内存;
- 将其中一个
add函数改名即可解决问题。
我想了解:
- 这是否是违反C++规则导致的预期行为?
- 我的这种实现方式是否不符合C++惯用写法,才导致这类问题未被广泛关注?
问题解答
1. 是否违反C++规则?
这并不违反C++规则,本质是编译器在模板重载解析时陷入了无限递归推导的死循环。
当调用c.add("1", Derived1())时,编译器会匹配最合适的add函数:
- 第一个非模板
add要求第二个参数是std::unique_ptr<Base>,Derived1对象无法直接隐式转换为该类型,所以这个重载的匹配优先级较低; - 第二个模板
add会被实例化,此时T被推导为Derived1。模板函数内部调用this->add(name, std::make_unique<T>(std::move(elem))),这里std::make_unique<Derived1>返回的是std::unique_ptr<Derived1>。
关键问题出在:编译器会认为std::unique_ptr<Derived1>还能匹配模板版本的add——因为它是一个新的类型,于是尝试实例化add<std::unique_ptr<Derived1>>,而这个实例化后的函数内部又会调用add,传入std::make_unique<std::unique_ptr<Derived1>>,以此类推陷入无限的模板实例化循环。
这属于编译器实现的疏漏(尤其是Clang对这种递归推导的终止检查不足),但代码本身并未违反C++标准,只是触发了编译器的极端处理场景。
2. 是否不符合C++惯用写法?
这种实现确实不符合C++的惯用思路,核心问题在于重载的模板函数未做类型约束,容易引发歧义或递归推导:
- 模板版本的
add没有限制T的类型,理论上能接受任何类型(包括智能指针本身),这就埋下了递归隐患; - 常规写法会给模板函数添加类型约束,比如要求
T是Base的派生类且不是智能指针类型,避免不必要的重载匹配。
改进方案
可以通过以下方式修复这个问题:
方案1:给模板函数添加类型约束(C++20)
使用concept限制T必须是Base的派生类,且不是智能指针:
#include <concepts> template<typename T> requires std::derived_from<T, Base> && !std::is_pointer_v<T> && !std::is_same_v<T, std::unique_ptr<Base>> void add(std::string name, T elem) { this->add(name, std::make_unique<T>(std::move(elem))); }
方案2:将模板函数改名
直接避免重载匹配的问题,比如改成add_object:
template<typename T> void add_object(std::string name, T elem) { this->add(name, std::make_unique<T>(std::move(elem))); } // 调用时使用 c.add_object("1", Derived1());
方案3:使用SFINAE(C++20之前)
通过enable_if限制模板参数的类型:
template<typename T> std::enable_if_t<std::is_base_of_v<Base, T> && !std::is_same_v<T, std::unique_ptr<Base>>> add(std::string name, T elem) { this->add(name, std::make_unique<T>(std::move(elem))); }
这些改进都能避免编译器陷入无限递归的模板实例化,同时让代码意图更清晰,符合C++的惯用设计思路。
内容的提问来源于stack exchange,提问作者lordlol

