C++ requires子句不满足时为何未跳过成员函数模板实例化
我正在编写仿STL的红黑树用于编码练习,实现的TreeSet、TreeMap、TreeMultiSet、TreeMultiMap均复用RedBlackTree的实现,类声明如下:
template <Containable K, typename V, typename Comp, bool AllowDup> requires std::invocable<Comp, K, K> class RedBlackTree { // ... }; template <Containable K, typename Comp = std::less<K>> using TreeSet = RedBlackTree<K, K, Comp, false>; template <Containable K, typename Comp = std::less<K>> using TreeMultiSet = RedBlackTree<K, K, Comp, true>; template <Containable K, Containable V, typename Comp = std::less<K>> using TreeMap = RedBlackTree<K, std::pair<const K, V>, Comp, false>; template <Containable K, Containable V, typename Comp = std::less<K>> using TreeMultiMap = RedBlackTree<K, std::pair<const K, V>, Comp, true>;
编写operator[]时需要保证该运算符仅为TreeMap实例化(与STL设计一致,四个有序关联容器中仅std::map提供operator[]),最初的operator[]声明如下:
template <typename T> std::add_lvalue_reference_t<decltype(std::declval<V>().second)> operator[](T&& raw_key) requires (!std::is_same_v<K, V> && !AllowDup)
但当RedBlackTree实例化为TreeSet时(此时V = K,!std::is_same_v<K, V>条件不成立)编译失败,编译器报错:实例化RedBlackTree为TreeSet<int>时V = K = int,V类型不存在second成员。
使用的编译器版本为MSVC 19.30,疑惑点是:为什么requires子句不满足的情况下,编译器没有跳过该函数的实例化?
核心原因是C++模板实例化规则中,类模板的成员函数声明(包括返回类型)如果不依赖于成员函数自身的模板参数,会在类模板实例化时被立刻解析,这个阶段早于成员函数的重载决议、requires约束检查阶段。
原代码中返回类型std::add_lvalue_reference_t<decltype(std::declval<V>().second)>里的V是外层类模板的参数,不依赖于operator[]自身的模板参数T:
- 当实例化
TreeSet<int>也就是RedBlackTree<int, int, std::less<int>, false>时,编译器会立刻解析类内所有成员函数的声明,此时就会尝试计算operator[]的返回类型,发现int类型没有second成员,直接触发硬编译错误。 - requires约束的检查发生在后续函数调用的重载决议阶段,在这之前解析返回类型的错误已经发生,自然不会被requires拦截。
- 这类错误不属于SFINAE(替换失败不是错误)可以处理的「即时上下文替换失败」范畴,属于类实例化阶段的硬错误。
只需要让operator[]的返回类型依赖于该成员函数自身的模板参数,强制将返回类型的推导延迟到重载决议、约束检查阶段即可,修改示例如下:
template <typename T, typename V_ = V> auto operator[](T&& raw_key) -> std::add_lvalue_reference_t<decltype(std::declval<V_>().second)> requires (!std::is_same_v<K, V_> && !AllowDup) { // 原有operator[]实现逻辑 }
这里新增了成员函数模板的默认类型参数V_ = V,让返回类型依赖于函数自身的模板参数:
- 类实例化阶段不会立刻推导依赖于函数模板参数的返回类型
- 当调用
operator[]触发重载决议时,会先检查requires约束,如果约束不满足,该重载会被直接排除,不会进入返回类型的替换推导步骤,也就不会触发访问second成员的错误。
内容的提问来源于stack exchange,提问作者frozenca

