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_ifsortcount 等。

问题在于,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 产生问题的深层原因在于 IncompleteHolder<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 算法的行为能够更好地保持一致性,用户无须采取任何措施,代码可以自动向后兼容。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.