3

假设我正在lib命名空间中编写一些通用算法,该算法调用自定义点my_func

my_func 第一次尝试是为想要专门针对他的类型的用户使用 ADL my_func,这是std类型的别名。当然在他的命名空间中定义它是行不通的,因为 ADL 不适用于别名。std标准不允许在命名空间中定义它。剩下的唯一选项似乎在算法的命名空间中定义lib。但是,如果最终用户在包含自定义标头之前包含算法标头,这也不起作用。

#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{});
}

https://godbolt.org/z/bfdP8s

第二次尝试是使用 niebloids my_func,这与 ADL 有相同的问题。

第三次尝试是 using tag_invoke,它应该与 ADL 有相同的问题,即

  • 在用户命名空间中自定义不起作用,因为我的类型是要std键入的别名
  • std不允许自定义
  • 命名空间中的自定义lib取决于标题包含的顺序 第一点似乎是正确的,但最后一点不是。这似乎有效
#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{});
}

https://godbolt.org/z/hsKbKE

为什么这与第一个(原始 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{});
}

https://godbolt.org/z/r71x6c


编写通用算法和自定义点并允许客户端自定义 std 类型的别名的最佳方法是什么?

4

1 回答 1

3

其中一位用户想要专门my_func针对他的类型,这是 std 类型的别名

这是原罪,它给你带来了所有的痛苦。C++ 中的类型别名只是别名;它们不是新类型。您有一个使用自定义点的通用算法,例如

// 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) which调用lib_ti::tag_invoke(*this, t),tag_invoke(lib::my_func, t)对此调用的关联命名空间)。这就是为什么它会发现你放入的重载。tlib::my_func_fnlibtag_invokenamespace lib

在原始 ADL 情况下,namespace lib不是调用的关联命名空间my_func(t)my_func找不到您放入的重载namespace lib,因为 ADL 没有找到它(不在关联的命名空间中),也没有通过常规的非限定查找找到它(因为挥手模糊的两阶段查找)。


编写通用算法和自定义点并允许客户端自定义 std 类型的别名的最佳方法是什么?

不。类型的“接口”——它支持什么操作,你可以用它做什么——是在类型作者的控制之下。如果您不是该类型的作者,请不要对其添加操作;相反,创建您自己的类型(可能通过继承,最好通过组合)并为其提供您想要的任何操作。

在最坏的情况下,你最终会在程序的不同部分有两个不同的用户,一个在做

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;
    }
};

然后当您将 a从一个目标文件传递到另一个目标文件并且他们同意名称但不同意其含义时,他们都尝试使用std::unordered_set<IntSet>, 然后在运行时使用繁荣、ODR 违规和未定义行为。这只是一个巨大的蠕虫罐头。不要打开它。std::unordered_set<IntSet>std::hash<std::set<int>>

于 2020-12-16T20:59:10.943 回答