std 类型别名的自定义点
customisation point for alias to std types
假设我正在 lib
命名空间中编写一些通用算法,调用自定义点 my_func
。
第一次尝试使用 ADL my_func
其中一位用户想要专门化 my_func
他的类型,这是 std
类型的别名。当然在他的命名空间中定义它是行不通的,因为 ADL 对别名不起作用。标准不允许在 std
命名空间中定义它。剩下的唯一选项似乎在算法的命名空间 lib
中定义。但是,如果最终用户在包含自定义 header.
之前包含算法 header,这也不起作用
#include <iostream>
#include <array>
// my_algorithm.hpp
namespace lib{
template<typename T>
void my_algorithm(const T& t){
my_func(t);
}
} // namespace lib
// user1.hpp
namespace user1{
struct Foo1{
// this is working as expected (ADL)
friend void my_func(const Foo1&){
std::cout << "called user1's customisation\n";
}
};
} // namespace user1
// user2.hpp
namespace user2{
using Foo2 = std::array<int,1>;
// this won't work because Foo2 is actually in std namespace
void my_func(const Foo2&){
std::cout << "called user2's customisation\n";
}
} // namespace user2
/* surely this isn't allowed
namespace std{
void my_func(const user2::Foo2&){
std::cout << "called user2's customisation\n";
}
} //namespace std
*/
// another attempt to costomize in the algorithm's namespace
// this won't work because my_func isn't seen before my_algorithm
namespace lib{
void my_func(const user2::Foo2&){
std::cout << "called user2's customisation\n";
}
}
// main.cpp
// #include "algorithm.hpp"
// #include "user1.hpp"
// #include "user2.hpp"
int main(){
lib::my_algorithm(user1::Foo1{});
lib::my_algorithm(user2::Foo2{});
}
第二次尝试使用 niebloids my_func
,这与 ADL 存在相同的问题。
第三次尝试使用 tag_invoke
,它应该与 ADL 存在相同的问题,即
- 在用户命名空间中自定义将不起作用,因为我的类型是
std
类型 的别名
- 不允许在
std
中进行自定义
- 在
lib
命名空间中的自定义取决于 header 包含的顺序
第一点似乎是正确的,但最后一点不是。这似乎有效
#include <iostream>
#include <array>
// tag_invoke.hpp overly simplified version
namespace lib_ti{
inline namespace tag_invoke_impl{
inline constexpr struct tag_invoke_fn{
template<typename CP, typename... Args>
decltype(auto) operator()(CP cp, Args&&... args) const{
return tag_invoke(cp, static_cast<Args&&>(args)...);
}
} tag_invoke{};
} // namespace tag_invoke_impl
} // namespace lib_to
// my_algorithm.hpp
// #include "tag_invoke.hpp"
namespace lib{
inline constexpr struct my_func_fn {
template <typename T>
void operator()(const T& t) const{
lib_ti::tag_invoke(*this, t);
}
} my_func{};
template<typename T>
void my_algorithm(const T& t){
my_func(t);
}
} // namespace lib
// user1.hpp
namespace user1{
struct Foo1{
// this is working as expected (ADL)
friend void tag_invoke(lib::my_func_fn, const Foo1&){
std::cout << "called user1's customisation\n";
}
};
} // namespace user1
// user2.hpp
namespace user2{
using Foo2 = std::array<int,1>;
// this won't work because Foo2 is actually in std namespace
void tag_invoke(lib::my_func_fn, const Foo2&){
std::cout << "called user2's customisation\n";
}
} // namespace user2
/* surely this isn't allowed
namespace std{
void tag_invoke(lib::my_func_fn, const user2::Foo2&){
std::cout << "called user2's customisation\n";
}
} //namespace std
*/
// another attempt to customise in the algorithm's namespace
// In ADL case, this does not work. But in this case, it seems to work. why?
namespace lib{
void tag_invoke(lib::my_func_fn, const user2::Foo2&){
std::cout << "called user2's customisation\n";
}
}
// main.cpp
int main(){
lib::my_algorithm(user1::Foo1{});
lib::my_algorithm(user2::Foo2{});
}
为什么这与第一个(原始 ADL)没有相同的问题?
第四次尝试使用模板专业化,这似乎按预期正常工作
#include <iostream>
#include <array>
// my_algorithm.hpp
namespace lib{
template<typename T, typename = void>
struct my_func_impl{
//void static apply(const T&) = delete;
};
inline constexpr struct my_func_fn {
template <typename T>
void operator()(const T& t) const{
using impl = my_func_impl<std::decay_t<T>>;
impl::apply(t);
}
} my_func{};
template<typename T>
void my_algorithm(const T& t){
my_func(t);
}
} // namespace lib
// user1.hpp
namespace user1{
struct Foo1{};
} // namespace user1
namespace lib{
template<>
struct my_func_impl<user1::Foo1>{
void static apply(const user1::Foo1&){
std::cout << "called user1's customisation\n";
}
};
} //namespace lib
// user2.hpp
namespace user2{
using Foo2 = std::array<int,1>;
} // namespace user2
namespace lib{
template<>
struct my_func_impl<user2::Foo2>{
void static apply(const user2::Foo2&){
std::cout << "called user2's customisation\n";
}
};
}
// main.cpp
int main(){
lib::my_algorithm(user1::Foo1{});
lib::my_algorithm(user2::Foo2{});
}
编写通用算法和自定义点并允许客户自定义 std 类型的别名的最佳方法是什么?
one of the user wants to specialise my_func
for his type, which is an alias to std type
这就是原罪,给你带来了所有的痛苦。 C++ 中的类型别名只是 aliases;它们不是新类型。您有一个使用自定义点的通用算法,例如
// stringify_pair is my generic algorithm; operator<< is my customization point
template<class T>
std::string stringify_pair(K key, V value) {
std::ostringstream oss;
oss << key << ':' << value;
return std::move(oss).str();
}
您的用户希望使用标准类型调用此泛型算法,例如
std::string mykey = "abc";
std::optional<int> myvalue = 42;
std::cout << stringify_pair(mykey, myvalue);
这不起作用,因为 std::optional<int>
没有提供 operator<<
。
它不可能工作,因为您的用户不拥有 std::optional<int>
类型,因此无法添加对它的操作。 (他们当然可以 尝试 ,从物理上讲;但从哲学的角度来看它是行不通的,这就是为什么每次你得到 运行 进入障碍(物理上) 关闭。)
用户使他们的代码工作的最简单方法是让他们“获得类型定义的合法所有权”,而不是依赖其他人的类型。
struct OptionalInt {
std::optional<int> data_;
OptionalInt(int x) : data_(x) {}
friend std::ostream& operator<<(std::ostream&, const OptionalInt&);
};
OptionalInt myvalue = 42; // no problem now
你问为什么 tag_invoke
没有与原始 ADL 相同的问题。我相信答案是,当您调用 lib::my_func(t)
时,它调用 lib_ti::tag_invoke(*this, t)
,它对 tag_invoke(lib::my_func, t)
执行 ADL 调用,它正在使用包含您的 t
的参数列表执行 ADL ] (这并不重要) 和 类型 lib::my_func_fn
的第一个参数(这意味着 lib
是此调用的关联命名空间)。这就是为什么它会找到您放入 namespace lib
.
的 tag_invoke
重载的原因
在原始 ADL 情况下,namespace lib
是 而不是 调用 my_func(t)
的关联命名空间。未找到您放入 namespace lib
的 my_func
重载,因为它未被 ADL 找到(不在关联的命名空间中)并且也未被常规不合格查找找到(因为 含糊地摆摆手二相查找).
What is the best way to write generic algorithms and customisation points and allow clients to customise for aliases for std types?
不要。类型的“接口”——它支持什么操作,你可以用它做什么——在类型作者的控制之下。如果您不是该类型的作者,请不要向其添加操作;相反,创建你自己的类型(可能通过继承,最好通过组合)并给它任何你想要的操作。
在最坏的情况下,您最终会在程序的不同部分遇到两个不同的用户,一个在做
using IntSet = std::set<int>;
template<> struct std::hash<IntSet> {
size_t operator()(const IntSet& s) const { return s.size(); }
};
另一个正在做
using IntSet = std::set<int>;
template<> struct std::hash<IntSet> {
size_t operator()(const IntSet& s, size_t h = 0) const {
for (int i : s) h += std::hash<int>()(i);
return h;
}
};
然后他们都尝试使用 std::unordered_set<IntSet>
,然后 boom,当你从一个目标文件到另一个目标文件,他们同意 std::hash<std::set<int>>
的名称但不同意其含义。这只是一大堆蠕虫。不要打开它。
假设我正在 lib
命名空间中编写一些通用算法,调用自定义点 my_func
。
第一次尝试使用 ADL my_func
其中一位用户想要专门化 my_func
他的类型,这是 std
类型的别名。当然在他的命名空间中定义它是行不通的,因为 ADL 对别名不起作用。标准不允许在 std
命名空间中定义它。剩下的唯一选项似乎在算法的命名空间 lib
中定义。但是,如果最终用户在包含自定义 header.
#include <iostream>
#include <array>
// my_algorithm.hpp
namespace lib{
template<typename T>
void my_algorithm(const T& t){
my_func(t);
}
} // namespace lib
// user1.hpp
namespace user1{
struct Foo1{
// this is working as expected (ADL)
friend void my_func(const Foo1&){
std::cout << "called user1's customisation\n";
}
};
} // namespace user1
// user2.hpp
namespace user2{
using Foo2 = std::array<int,1>;
// this won't work because Foo2 is actually in std namespace
void my_func(const Foo2&){
std::cout << "called user2's customisation\n";
}
} // namespace user2
/* surely this isn't allowed
namespace std{
void my_func(const user2::Foo2&){
std::cout << "called user2's customisation\n";
}
} //namespace std
*/
// another attempt to costomize in the algorithm's namespace
// this won't work because my_func isn't seen before my_algorithm
namespace lib{
void my_func(const user2::Foo2&){
std::cout << "called user2's customisation\n";
}
}
// main.cpp
// #include "algorithm.hpp"
// #include "user1.hpp"
// #include "user2.hpp"
int main(){
lib::my_algorithm(user1::Foo1{});
lib::my_algorithm(user2::Foo2{});
}
第二次尝试使用 niebloids my_func
,这与 ADL 存在相同的问题。
第三次尝试使用 tag_invoke
,它应该与 ADL 存在相同的问题,即
- 在用户命名空间中自定义将不起作用,因为我的类型是
std
类型 的别名
- 不允许在
std
中进行自定义 - 在
lib
命名空间中的自定义取决于 header 包含的顺序 第一点似乎是正确的,但最后一点不是。这似乎有效
#include <iostream>
#include <array>
// tag_invoke.hpp overly simplified version
namespace lib_ti{
inline namespace tag_invoke_impl{
inline constexpr struct tag_invoke_fn{
template<typename CP, typename... Args>
decltype(auto) operator()(CP cp, Args&&... args) const{
return tag_invoke(cp, static_cast<Args&&>(args)...);
}
} tag_invoke{};
} // namespace tag_invoke_impl
} // namespace lib_to
// my_algorithm.hpp
// #include "tag_invoke.hpp"
namespace lib{
inline constexpr struct my_func_fn {
template <typename T>
void operator()(const T& t) const{
lib_ti::tag_invoke(*this, t);
}
} my_func{};
template<typename T>
void my_algorithm(const T& t){
my_func(t);
}
} // namespace lib
// user1.hpp
namespace user1{
struct Foo1{
// this is working as expected (ADL)
friend void tag_invoke(lib::my_func_fn, const Foo1&){
std::cout << "called user1's customisation\n";
}
};
} // namespace user1
// user2.hpp
namespace user2{
using Foo2 = std::array<int,1>;
// this won't work because Foo2 is actually in std namespace
void tag_invoke(lib::my_func_fn, const Foo2&){
std::cout << "called user2's customisation\n";
}
} // namespace user2
/* surely this isn't allowed
namespace std{
void tag_invoke(lib::my_func_fn, const user2::Foo2&){
std::cout << "called user2's customisation\n";
}
} //namespace std
*/
// another attempt to customise in the algorithm's namespace
// In ADL case, this does not work. But in this case, it seems to work. why?
namespace lib{
void tag_invoke(lib::my_func_fn, const user2::Foo2&){
std::cout << "called user2's customisation\n";
}
}
// main.cpp
int main(){
lib::my_algorithm(user1::Foo1{});
lib::my_algorithm(user2::Foo2{});
}
为什么这与第一个(原始 ADL)没有相同的问题?
第四次尝试使用模板专业化,这似乎按预期正常工作
#include <iostream>
#include <array>
// my_algorithm.hpp
namespace lib{
template<typename T, typename = void>
struct my_func_impl{
//void static apply(const T&) = delete;
};
inline constexpr struct my_func_fn {
template <typename T>
void operator()(const T& t) const{
using impl = my_func_impl<std::decay_t<T>>;
impl::apply(t);
}
} my_func{};
template<typename T>
void my_algorithm(const T& t){
my_func(t);
}
} // namespace lib
// user1.hpp
namespace user1{
struct Foo1{};
} // namespace user1
namespace lib{
template<>
struct my_func_impl<user1::Foo1>{
void static apply(const user1::Foo1&){
std::cout << "called user1's customisation\n";
}
};
} //namespace lib
// user2.hpp
namespace user2{
using Foo2 = std::array<int,1>;
} // namespace user2
namespace lib{
template<>
struct my_func_impl<user2::Foo2>{
void static apply(const user2::Foo2&){
std::cout << "called user2's customisation\n";
}
};
}
// main.cpp
int main(){
lib::my_algorithm(user1::Foo1{});
lib::my_algorithm(user2::Foo2{});
}
编写通用算法和自定义点并允许客户自定义 std 类型的别名的最佳方法是什么?
one of the user wants to specialise
my_func
for his type, which is an alias to std type
这就是原罪,给你带来了所有的痛苦。 C++ 中的类型别名只是 aliases;它们不是新类型。您有一个使用自定义点的通用算法,例如
// stringify_pair is my generic algorithm; operator<< is my customization point
template<class T>
std::string stringify_pair(K key, V value) {
std::ostringstream oss;
oss << key << ':' << value;
return std::move(oss).str();
}
您的用户希望使用标准类型调用此泛型算法,例如
std::string mykey = "abc";
std::optional<int> myvalue = 42;
std::cout << stringify_pair(mykey, myvalue);
这不起作用,因为 std::optional<int>
没有提供 operator<<
。
它不可能工作,因为您的用户不拥有 std::optional<int>
类型,因此无法添加对它的操作。 (他们当然可以 尝试 ,从物理上讲;但从哲学的角度来看它是行不通的,这就是为什么每次你得到 运行 进入障碍(物理上) 关闭。)
用户使他们的代码工作的最简单方法是让他们“获得类型定义的合法所有权”,而不是依赖其他人的类型。
struct OptionalInt {
std::optional<int> data_;
OptionalInt(int x) : data_(x) {}
friend std::ostream& operator<<(std::ostream&, const OptionalInt&);
};
OptionalInt myvalue = 42; // no problem now
你问为什么 tag_invoke
没有与原始 ADL 相同的问题。我相信答案是,当您调用 lib::my_func(t)
时,它调用 lib_ti::tag_invoke(*this, t)
,它对 tag_invoke(lib::my_func, t)
执行 ADL 调用,它正在使用包含您的 t
的参数列表执行 ADL ] (这并不重要) 和 类型 lib::my_func_fn
的第一个参数(这意味着 lib
是此调用的关联命名空间)。这就是为什么它会找到您放入 namespace lib
.
tag_invoke
重载的原因
在原始 ADL 情况下,namespace lib
是 而不是 调用 my_func(t)
的关联命名空间。未找到您放入 namespace lib
的 my_func
重载,因为它未被 ADL 找到(不在关联的命名空间中)并且也未被常规不合格查找找到(因为 含糊地摆摆手二相查找).
What is the best way to write generic algorithms and customisation points and allow clients to customise for aliases for std types?
不要。类型的“接口”——它支持什么操作,你可以用它做什么——在类型作者的控制之下。如果您不是该类型的作者,请不要向其添加操作;相反,创建你自己的类型(可能通过继承,最好通过组合)并给它任何你想要的操作。
在最坏的情况下,您最终会在程序的不同部分遇到两个不同的用户,一个在做
using IntSet = std::set<int>;
template<> struct std::hash<IntSet> {
size_t operator()(const IntSet& s) const { return s.size(); }
};
另一个正在做
using IntSet = std::set<int>;
template<> struct std::hash<IntSet> {
size_t operator()(const IntSet& s, size_t h = 0) const {
for (int i : s) h += std::hash<int>()(i);
return h;
}
};
然后他们都尝试使用 std::unordered_set<IntSet>
,然后 boom,当你从一个目标文件到另一个目标文件,他们同意 std::hash<std::set<int>>
的名称但不同意其含义。这只是一大堆蠕虫。不要打开它。