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

Допустим, я пишу некий общий алгоритм в пространстве имен 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


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

Пожалуйста, укажите соответствующий код в вопросе

463035818_is_not_a_number 14.12.2020 11:51
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
1
286
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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>>, но расходятся в его значении. Это просто огромная банка червей. Не открывай его.

Другие вопросы по теме