Допустим, я пишу некий общий алгоритм в пространстве имен 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{});
}
Вторая попытка — использовать 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{});
}
Почему у этого нет той же проблемы, что и у первого (необработанный 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{});
}
Как лучше всего написать общие алгоритмы и точки настройки и разрешить клиентам настраивать псевдонимы для стандартных типов?
один из пользователей хочет специализироваться
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>>
, но расходятся в его значении. Это просто огромная банка червей. Не открывай его.
Пожалуйста, укажите соответствующий код в вопросе