C++26 Finally Makes std::projected ADL-Proof
std::projected
是 C++20 引入的一个模板工具,用于获取应用投影函数后迭代器所指向的值类型。例如:
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};
auto proj = [](const Person& p) { return p.age; };
using ProjAge = std::projected<std::vector<Person>::iterator, decltype(proj)>;
static_assert(std::is_same_v<ProjAge::value_type, int>);
}
投影是 Ranges 的核心特性,投影函数就是一个可调用对象,许多 Ranges 算法都允许指定一个投影函数来访问复杂数据成员中的某一个元素,如 find_if
、sort
、count
等。
问题在于,C++20 std::projected
的实现没有做 ADL 防护,导致这些依赖它所实现的 Ranges 算法在针对不完整类型时会触发 Hard Error,而传统 STL 算法并不存在这一问题。如:
template<class T> struct Holder { T t; };
struct Incomplete;
Holder<Incomplete> *a[10] = {}; // ten null pointers
assert(std::count(a, a+10, nullptr) == 10); // OK
assert(std::ranges::count(a, a+10, nullptr) == 10); // hard error
这是因 ADL 机制产生的不必要的过早检查而导致的错误,因此,C++26 采用一些技巧修复了 std::projected
的实现,使其变为 ADL 安全的代码。
下面的示例能够更好地对比 ADL 导致的问题:
void __private_foo(...) {}
int main() {
Holder<Incomplete> *p = nullptr;
::__private_foo(p); // OK.
(__private_foo)(p); // OK.
__private_foo(p); // Error: Incomplete is incomplete.
}
__
命名开头的函数表示标准内部的实现,用户不会去定制这样的函数,因此 ADL 肯定无法找到任何相关的定制实现,但编译器不知道,还是会触发 ADL 机制去查找名称,从而导致 Hard Error。我们可以使用 ::
指定 Qualified 名称查找,或是使用 ()
阻止 ADL 来避免编译错误,但这些方法都需要逼迫调用方修改代码,向后兼容性不足。
这种场景下,ADL 产生问题的深层原因在于 Incomplete
是 Holder<Incomplete>
的关联实体,具体到 std::projected<T*, identity>
,T
是其关联实体,而编译器需要检查关联实体的类型完整性。
因此,解决方法就是令 T
不再是 std::projected<T*, identity>
的关联实体,另一种阻止 ADL 的巧妙手法 对这一手法作了详细的讲解。于是,将:
template<class Associated>
struct projected {};
实现变为:
template<class T>
struct __projected_impl {
struct type { };
};
template<class NonAssociated>
using projected = __projected_impl<NonAssociated>::type;
此时,__projected_impl<NonAssociated>::type
的关联实体为 __projected_impl<NonAssociated>
,因为这种 ADL 查找不具备可传递性,所以不会查找到 NonAssociated
,也就不会检查其完整性。
有了这种 ADL 防护,下面这些代码将不会再产生 Hard Error:
using T = Holder<Incomplete>*;
static_assert(std::equality_comparable<T>); // OK
static_assert(std::indirectly_comparable<T*, T*, std::equal_to<>>); // will be OK
static_assert(std::sortable<T*>); // will be OK
int main() {
Holder<Incomplete> *a[10] = {}; // ten null pointers
assert(std::count(a, a+10, nullptr) == 10); // OK
assert(std::ranges::count(a, a+10, nullptr) == 10); // will be OK
}
由此,传统的 STL 算法与 Ranges 算法的行为能够更好地保持一致性,用户无须采取任何措施,代码可以自动向后兼容。