Комментарии 31
template <Impl::HasStdConversion Numeric> std::string makeString(Numeric value)
Code review: Для шаблонных параметров всегда используй ссылки, если специально не нужно другое
В общем случае - согласен, в данном случае для обертки вокруг std::to_string
ссылка не нужна, т. к. у нас гарантированно примитивный тип, который дёшево копируется.
Где ты это гарантируешь?
Стандартом гарантированы 9 перегрузок std::to_string, принимающих примитивный тип по копии. Любой другой std::to_string приводит к undefined behavior: в моем случае это будет лишнее копирование.
Можно было бы подстраховаться, добавив еще одно требование к шаблону, по типу is_numeric, но для краткости, думаю, и так сойдет. Тем более, я старался вводить новые подходы в статью по одному и с объяснением задачи, которую он решает.
В общем случае, надо просто соблюдать простые правила, например - передавать шаблонные параметры по ссылке. Тогда сложные случаи может и не понадобятся.
А зачем вы искусственно запретили инлайнинг? :) Если убрать атрибут [[gnu::noinline]]
у foo1()
, то никакой разницы не будет, абсолютно. Вы же понимаете, что шаблонная функция с искусственно выключенной возможностью ее инлайнинга - это скорее исключение, чем правило?
template <typename T> requires requires (T a) {{std::to_string(a)} -> std::convertible_to<std::string>;}
std::string makeString(T &&value)
{
return std::to_string(std::forward<T>(value));
}
Если ту, что буква греческого алфавита, то да!
Но вообще, я изначально хотел запретить makeString(char)
из-за его неочевидного поведения, но сначала не стал потому, что хотел вводить новые техники постепенно, потом - потому, что из этого вырос целый раздел про CppInsight раскапывание причин непонятного поведения компилятора. А потом забыл.
template <typename T> requires std::is_same_v<T, char>
std::string makeString(T value)
Для реализации makeString(Numeric)
можно добавить if constexpr и вообще как шестнадцатеричные цифры это выводить, т.к. массив симвовов у нас строкой не считается.
Или для makeString(Iterable)
сделать вариант шаблона, который будет выводить такой массив в виде base64.
В любом случае, это уже частные решения вывода массивов 1-байтных целых в строку. основной идеей было показать trait-ы на практическом примере и CppInsight.
При этом полно примеров, когда компилятор действительно идёт по ложному пути и генерирует запутанные или совершенно не связанные с реальной ошибкой сообщения. Например, какая-нибудь опечатка приводит к сообщению о семантической ошибке с упоминанием всей цепочки вызовов шаблонного класса и километровыми составными именами, хотя код синтаксически не верен.
Концепты и ограничения (constraints) – синтаксический сахар для SFINAE
Категорически не согласен. Вообще, если взглянуть на код в статье, то любого опытного человека начинают терзать смутные сомнения. Предположим, что мы написали библиотеку, в которй содержится функция makeString() и которую пользователь может расширять своими типами и даже семействами типов. А пользователь хочет добавить поддержку вот таких вот шаблонов:
template <typename T>
struct harry
{
std::vector<T> m_vct;
harry(std::initializer_list<T> vct) : m_vct{ vct } {}
auto begin() { return m_vct.begin(); }
auto begin() const { return m_vct.begin(); }
auto end() { return m_vct.end(); }
auto end() const { return m_vct.end(); }
auto to_string() const { return "avada"; }
};
template <typename T>
struct potter
{
std::vector<T> m_vct;
potter(std::initializer_list<T> vct) : m_vct{ vct } {}
auto begin() { return m_vct.begin(); }
auto begin() const { return m_vct.begin(); }
auto end() { return m_vct.end(); }
auto end() const { return m_vct.end(); }
auto to_string() const { return "kedavra"; }
};
Как написать шаблон функции makeString(), чтобы для этих шаблонов классов использовалась функция класса to_string() как для объектов, а не итерирование, как для контейнеров. Напомню, что код в makeString.hpp - библиотечный, значит его править нельзя. Может быть так?
template <typename Magic>
requires (Impl::HasToString<Magic> && Impl::IsContainer<Magic>)
std::string makeString(const Magic& magic)
{
return magic.to_string();
}
Нет, так получится клэш с функцией для контейнеров. Не правя "библиотечный" код, видимо, придётся выписывать для каждого шаблона отдельную функцию. И тоже самое верно для SFINAE. Но, оказывается, есть более простой, элегантный и расширяемый метод написания такой "библиотечной функции" на концептах, если следовать философии концептов. Мы можем выписать используемые здесь ограничения в нормализованном виде
Numeric = HasStdConversion
Object = HasToString
Container = IsContainer
String = IsContainer && IsString
Тогда ограничение на Magic можно записать так
Magic = IsContainer && HasToString
Обращу внимание вот на что. При выборе функци кандидата для объекта типа harry<int> функции для Numeric и String не подходят, а вот Object и Container - подходят. Но Magic - более ограничивающий концепт, чем Object и Container, поэтому выбирается функция с ограничением Magic (см. Partial ordering of constraints). Переписываем ограничения на функции в требуемом виде.
template <Impl::IsContainer Container>
std::string makeString(Container &&iterable)
{
std::string result;
for (auto &&i : std::forward<Container>(iterable))
{
if (!result.empty())
result += ';';
result += makeString(std::forward<decltype(i)>(i));
}
return result;
}
template <typename String>
requires Impl::IsContainer<String> && Impl::IsString<String>
std::string makeString(String &&s)
{
return std::string(std::forward<String>(s));
}
template <typename Magic>
requires Impl::IsContainer<Magic> && Impl::HasToString<Magic>
std::string makeString(Magic &&magic)
{
return std::forward<Magic>(magic).to_string();
}
и получаем
#include <string>
#include <type_traits>
#include <concepts>
#include <iostream>
#include <set>
#include <vector>
namespace Impl
{
template <typename T>
concept IsString = std::is_convertible_v<T, std::string>
|| std::is_same_v<T, std::string_view>;
template <typename T>
concept HasToString = requires (const T & object)
{
{ object.to_string() } -> std::convertible_to<std::string>;
};
template <typename T>
concept HasStdConversion = requires (T number) { std::to_string(number); };
template <typename T>
concept IsContainer = requires (const T & container) { std::begin(container); };
}
template <Impl::HasToString Object>
std::string makeString(Object &&object)
{
return object.to_string();
}
template <Impl::HasStdConversion Numeric>
std::string makeString(Numeric value)
{
return std::to_string(value);
}
template <Impl::IsContainer Container>
std::string makeString(Container &&iterable)
{
std::string result;
for (auto &&i : std::forward<Container>(iterable))
{
if (!result.empty())
result += ';';
result += makeString(std::forward<decltype(i)>(i));
}
return result;
}
template <typename String>
requires Impl::IsContainer<String> && Impl::IsString<String>
std::string makeString(String &&s)
{
return std::string(std::forward<String>(s));
}
template <typename... Pack>
requires (sizeof...(Pack) > 1)
std::string makeString(Pack&&... pack)
{
return (... += makeString(std::forward<Pack>(pack)));
}
///////////////////////////////////////////////////////////////////////////////
struct A
{
std::string to_string() const { return "A"; }
};
struct B
{
int m_i = 0;
std::string to_string() const { return "B{" + std::to_string(m_i) + "}"; }
};
template <typename T>
struct harry
{
std::vector<T> m_vct;
harry(std::initializer_list<T> vct) : m_vct{ vct } {}
auto begin() { return m_vct.begin(); }
auto begin() const { return m_vct.begin(); }
auto end() { return m_vct.end(); }
auto end() const { return m_vct.end(); }
auto to_string() const { return "avada"; }
};
template <typename T>
struct potter
{
std::vector<T> m_vct;
potter(std::initializer_list<T> vct) : m_vct{ vct } {}
auto begin() { return m_vct.begin(); }
auto begin() const { return m_vct.begin(); }
auto end() { return m_vct.end(); }
auto end() const { return m_vct.end(); }
auto to_string() const { return "kedavra"; }
};
template <typename Magic>
requires Impl::IsContainer<Magic> && Impl::HasToString<Magic>
std::string makeString(Magic &&magic)
{
return std::forward<Magic>(magic).to_string();
}
int main()
{
A a;
B b = { 1 };
const std::vector<int> xs = { 1, 2, 3 };
const std::set<float> ys = { 4, 5, 6 };
const double zs[] = { 7, 8, 9 };
std::cout << "a: " << makeString(a) << "; b: " << makeString(b)
<< "; pi: " << makeString(3.14) << std::endl
<< "xs: " << makeString(xs) << "; ys: " << makeString(ys)
<< "; zs: " << makeString(zs)
<< std::endl;
std::cout << makeString("Hello, ")
<< makeString(std::string_view("world"))
<< makeString(std::string("!!1"))
<< std::endl;
const std::string constHello = "const hello!";
std::cout << makeString(constHello)
<< std::endl;
std::cout << makeString("xs: ", xs, "; and float is: ", 3.14f)
<< std::endl;
std::cout << makeString(harry{ 99, -1, 4 })
<< std::endl;
std::cout << makeString(potter{ 14, 88 })
<< std::endl;
}
и output
a: A; b: B{1}; pi: 3.140000
xs: 1;2;3; ys: 4.000000;5.000000;6.000000; zs: 7.000000;8.000000;9.000000
Hello, world!!1
const hello!
xs: 1;2;3; and float is: 3.140000
avada
kedavra
Кстати, при использовании partial ordering имеет смысл пользоваться одним типом ссылок в сигнатуре функции, чтобы не получать странные результаты.
Сегодня мы познакомились с любопытным, уникальным свойством концептов, позволяющем перегружать шаблоны функций и частичные специализации шаблонов классов способом, напоминающем перегрузку функций для типов.
Спасибо, писал про концепты на остатках ночиного энтузиазма и не разобрался с приоритетом применения.
Сегодня дописал код и статью для поддержки вашего примера.
Пожалуй можно было как то и без стены кода мимо проходящим людям рассказать, что компилятор С++ умеет доказывать небольшие теоремы, в частности доказывать что одно утверждение более ограниченное, чем другое, или наоборот
так оно работает на концептах, а не рандомных N > 0
построение хороших концептов лежит на плечах создающего концепты и это очень логически верно. Вместо того чтобы усложнять реализацию компилятора до невозможности с доказыванием произвольных утверждений, в том числе из комментариев разработчика, есть математически формальная система концептов.
template<int N>
concept null = N == 0;
template<int N>
concept positive = N > 0;
template<int N>
concept non_negative = null || positive;
template<int N>
void foo() { puts("<0"); }
template<int N>
requires positive<N>
void foo() { puts(">0"); }
template<int N>
requires non_negative<N>
void foo() { puts(">=0"); }
Кому то может показаться это сложнее, но на самом деле эта система проще, ведь если бы компилятор научился доказывать что ограниченнее N > 0 или N >=0 или более сложные вещи, то запутаться в том что происходит можно было бы за пару простейших выражений. Потому что сам программист тоже должен был бы тогда уметь всё это доказывать, чтобы понять какая функция будет вызвана
Конечно можно - вы справились! Моя задача была не в этом.
При работе с концептами лучше использовать std::same_as, чтобы не было неожиданостей.
Для компилятора в случае концептов is_same_v<T, U> и is_same_v<U, T> будут разными атомами, поэтому лучше заранее избежать проблем.
Тема раскрывается у Andrzej-я
Что значит std::to_string не расширяем новыми типами? Определяем to_string в неймспейсе нашего типа. Пишем ниеблоид в котором делаем
using std::to_string;
return to_string(std::forward<T>(t));
И мы великолепны
Непонятно, но очень интересно =)
https://stackoverflow.com/questions/62928396/what-is-a-niebloid
Спасибо за столь развёрнутую статью. Недавно я хотел написать свою версию шаблонного to_string, но слава богу наткнулся на вашу реализацию:) чувствовал что тут немало подводных камней:) С++ довольно сложный и многогранный язык. Уже за хорошую статью в этой области нужно всем сообществом сначала тысячу благодарственных отзывов написать. А только потом критиковать:) А то мотивация писать такие классные статьи не появится и у менее профессионалов.
О шаблонах в С++, чуть сложнее