точка настройки псевдонима для стандартных типов

Допустим, я пишу некий общий алгоритм в пространстве имен lib, который вызывает точку настройки my_func.

Первая попытка заключается в использовании ADL для my_func, один из пользователей хочет специализировать 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.

Третья попытка использует 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


Как лучше всего написать общие алгоритмы и точки настройки и разрешить клиентам настраивать псевдонимы для стандартных типов?


person Hui    schedule 14.12.2020    source источник
comment
Пожалуйста, укажите соответствующий код в вопросе   -  person 463035818_is_not_a_number    schedule 14.12.2020


Ответы (1)


один из пользователей хочет специализировать my_func для своего типа, который является псевдонимом стандартного типа

Это первородный грех, причиняющий вам всю боль. Псевдонимы типов в 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), который вызывает lib_ti::tag_invoke(*this, t), который выполняет вызов ADL к tag_invoke(lib::my_func, t), он выполняет ADL со списком аргументов, который включает как ваш t (что не имеет большого значения), так и и этот первый аргумент типа lib::my_func_fn (что означает, что lib является ассоциированным пространством имен для этого вызова). Вот почему он находит перегрузку tag_invoke, которую вы поместили в namespace lib.

В случае необработанного ADL namespace lib не является ассоциированным пространством имен вызова my_func(t). Перегрузка my_func, которую вы поместили в namespace lib, не найдена, потому что она не найдена ADL (не в связанном пространстве имен) и не найдена обычным неквалифицированным поиском (поскольку неопределенно машет руками два -фазовый поиск).


Как лучше всего написать общие алгоритмы и точки настройки и разрешить клиентам настраивать псевдонимы для стандартных типов?

Не. Интерфейс типа — какие операции он поддерживает, что с ним разрешено делать — находится под контролем автора типа. Если вы не являетесь автором типа, не добавляйте к нему операции; вместо этого создайте свой собственный тип (возможно, путем наследования, предпочтительно по составу) и назначьте ему любые операции, которые вы хотите.

В худшем случае вы получите двух разных пользователей в разных частях программы, один из которых делает

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>, а затем бум, нарушение ODR и неопределенное поведение во время выполнения, когда вы передаете std::unordered_set<IntSet> из одного объектного файла в другой, и они согласны с именем std::hash<std::set<int>>, но не согласны с его значение. Это просто огромная банка червей. Не открывай его.

person Quuxplusone    schedule 16.12.2020