О шаблонах в С++, чуть сложнее

После недавней статьи о шаблонах С++ для начинающих осталось жгучее желание показать что-нибудь похожее, но на практическом примере, да так, чтобы и порог входа был не высоким, и чтобы скучно не было. А так как в голове крутится задача перевода чего бы то ни было в строку, то этим и предлагаю заняться всем, кто хочет потрогать компилятор за шаблоны.

Оглавление

  1. Проблема и предлагаемое решение

  2. Постановка задачи

  3. Простой шаблон

  4. Специализация шаблона функции и перегрузка функций

  5. SFINAE (Substitution Failure Is Not An Error) — если подстановка не сработала, то её можно проигронировать

  6. SFINAE и trailing return type

  7. Пишем makeString() для коллекций

  8. Попытка написать makeString() для строк

  9. Type traits — свойства типов и специализация шаблонов по числовому параметру (by Non-type template parameter)

  10. Что же ты, ____ [компилятор], делаешь…

  11. Библиотека type_traits

  12. Универсальные ссылки и std::forward

  13. Variadic templates — шаблоны с переменным числом параметров

  14. Рекурсивный подход к variadic templates

  15. Подход со сверткой (fold expression) к Variadic templates

  16. Концепты и ограничения (constraints) — синтаксический сахар для SFINAE

  17. Стало ли с концептами лучше?

  18. Итоги

Проблема

В имеющемся std::to_string присутствует несколько недостаков:

  • он написан не нами. Другими словами, во время его написания опыт и море удовольствия были получены кем-то другим.

  • он не расширяем новыми типами. На практике, можно расширить пространство std своими перегрузками и даже шаблонами, но стандарт говорит о неопределенном поведении такого кода. Кроме того, подобные решения могут порождать слишком жаркие споры в курилке.

Предлагаемое решение

Напишем свой вариант makeString с отличными от std::to_string недостатками. Требованиями к нему будут:

  • схожий очевидный интерфейс: makeString(3.14); и makeString(directionVector); должны делать строку из своего аргумента;

  • расширяемость. Невозможно заранее знать, как перевести экземпляр произвоьлного класса class UserRequest; в строку, но можно сказать, что объект любого класса с методом std::string to_string() const; переводится в строку очевидным образом. Другими словами, для добавления поддержки перевода нового типа в строку, в нем нужно будет реализовать один метод to_string.

  • код писать будем на актуальной версии С++ который можно без проблем получить «из коробки» в актуальной на данный момент Ubuntu 20.04.3 LTS или VS 2019 Community Edition. А для того, чтобы это был чистый C++, попросим компилятор быть с нами построже. Я передам свою просьбу компилятору с помощью CMake-скрипта, и буду надеяться, чтоб абсолютному большинству С++ программистов не составит труда перевести эти руны в свою любимую IDE для своего любимого компилятора.

cmake_minimum_required(VERSION 3.0.0) project(makeString VERSION 0.1.0)  add_executable(makeString main.cpp)  set(CMAKE_CXX_EXTENSIONS OFF)      # no vendor-specific extensions set_property(TARGET makeString PROPERTY CXX_STANDARD 20)  if (MSVC)     target_compile_options(makeString PUBLIC /W4 /WX /Za /permissive-) else()     target_compile_options(makeString PUBLIC -Wall -Wextra -pedantic -Werror) endif()

Опыт и море удовольствия

И так, постановка задачи на языке С++:

#include <iostream> #include "makeString.hpp"  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) + "}"; } };  int main() {     A a;     B b = {1};      std::cout << "a: " << makeString(a) << "; b: " << makeString(b); }

Простой шаблон

Так как мы хотим, чтобы to_string у нас автоматичкски «подхватывался» из объекта пользовательсого типа, нам нужен шаблон. Шаблон — это вещь, на первый взгляд, нехитрая, но если уже здесь возникают сложности, то можно обратиться к указанной выше статье, где достаточно подробно и простым языком описаны основы.

// makeString.hpp #pragma once #include <string>  template <typename Object> std::string makeString(const Object& object) {     return object.to_string(); }

Собрали, запустили, работает: a: A; b: B{1}

Специализация шаблона функции и перегрузка функций

Хотелось бы иметь возможность написать makeString(3.14) несмотря на то, что у типа double нет метода to_string. К счастью, реализацию std::to_string у нас никто не забирал, и из прошлой статьи мы уже знаем про специализацию шаблонов функций.

int main() {   // ...      std::cout << "a: " << makeString(a) << "; b: " << makeString(b)                << "; pi: " << makeString(3.14)               << std::endl; }
// makeString.hpp ... // [build] ../makeString.hpp:13:24: error:  // template-id ‘makeString<>’ for ‘std::string makeString(double)’  // does not match any template declaration template <> std::string makeString(double d) { return std::to_string(d); }

Первый блин — комом. Действительно, компилятор не понимает, какой именно из шаблонов, принимающих константную ссылку, мы хотим специализировать этой сигнатурой. Освежив свои познания полезной статьей на cppreference, мы ему поможем, и понадеемся, что оптимизатор «подчистит» за нами передачу примитивных типов по ссылке. Так же вспомним, что кроме double у нас есть float и еще 7 других примитивных типов, для которых есть своя перегрузка std::to_string:

// makeString.hpp #pragma once #include <string>  template <typename Object> std::string makeString(const Object &object) {     return object.to_string(); }  template <> std::string makeString(const double& d) { return std::to_string(d); } template <> std::string makeString(const float& f)  { return std::to_string(f); } template <> std::string makeString(const int& i)    { return std::to_string(i); } // ... 6 more specializations ...

Есть, однако, и альтернативный вариант. Использование шаблонов не отменяет использование перегрузки функций, поэтому нет ничего плохого в том, чтобы просто добавить перегрузок. Краем глаза глянув раздел Function template overloading в замечательной статье на сайте, имя которого я не буду указывать, т.к. они не платят мне за SEO. Статья действительно хороша, можно плодотворно провести за её чтением не одни сутки.

А если читать некогда, будем писать код. Подход с перегрузками работающий, полезный, и выглядит лаконичнее пачки перегрузок.

// overloading instead of specializations std::string makeString(double d) { return std::to_string(d); } std::string makeString(float f)  { return std::to_string(f); } std::string makeString(int i)    { return std::to_string(i); } // ... 6 more overloads ...

Компилируем, запукскаем, заработало: a: A; b: B{1}; pi: 3.140000

Где-то посередине написания этой копипасты из перегрузок, разработчика может посетить мысль о том, что копипасту можно заменить её на еще один шаблон. Попробуем:

// makeString.hpp #pragma once #include <string>  template <typename Object> std::string makeString(const Object& object) {     return object.to_string(); }  /*   [build] ../main.cpp: In function ‘int main()’:   [build] ../main.cpp:20:39: error: call of overloaded ‘makeString(A&)’ is ambiguous   [build]    20 |     std::cout << "a: " << makeString(a) << "; b: " << makeString(b) */ template <typename Numeric> std::string makeString(Numeric value) {     return std::to_string(value); } 

И ведь действительно: есть вызов makeString(a), где a в месте вызова имеет тип A& (lvalue reference to A), и компилятор не понимает, какому из шаблонов надо эти вызовы сопоставить, т.к. синтаксически верна подстановка в оба объявления (declaration) шаблонной функции, а в определение (definition) шаблонной функции он во время подстановки лезть не должен и не будет.

В этот момент может появиться желание «выключить» одну из сигнатур из разрешения перегрузок. Желание есть, в нормы современной морали оно вписывается, значит не будем ему противиться, ведь у нас есть…

SFINAE (Substitution Failure Is Not An Error) — если подстановка не сработала, то её можно проигронировать

Кстати, у них (у тех, кто не платит мне за SEO), SFINAE тоже есть. Здесь же будет простое описание на случай, когда хочется не читать, а писать: если в результате подстановки шаблонных параметров в объявление шаблонной функции получается синтаксически неверная конструкция, то это объявление будет проигнорировано компилятором без сообщения об ошибке. То же самое справедливо и для шаблонных классов и переменных.

Другими словами, чтобы «выключить» один из шаблонов из перегрузки, нужно сделать так, чтобы для «выключамой» функции не удалось вычислить типы параметров или возвращаемого значения в месте её вызова. Проявив фантазию, энтузиазм и смекалку, можно прийти к нескольким вариантам, добавив в сигнатуры функций то, что не скомпилируется для тех типов, которые эта функция не поддерживает:

// makeString.hpp #pragma once #include <string> #include <utility>  // for std::declval  // (1) template <typename Object,            typename = decltype(std::declval<Object>().to_string())> std::string makeString(const Object& object) {     return object.to_string(); }  namespace Impl { bool acceptNumber(int); }  // (2) template <typename Numeric> std::string makeString(Numeric value,                         decltype(Impl::acceptNumber(value))* = nullptr) {     return std::to_string(value); }

Постараюсь расшифровать приведенные выше руны:

  1. Первая функция имеет два шаблонных параметра: тип объекта, и безымяный неиспользуемый параметр того же типа, который вернет вызов Object::to_string() для сферического экземпляра Object в вакууме. std::declval<Object> в данном случае — это замена конструктора по умолчанию, т.к. у типа Object такого конструктора может не быть.

    Для типов A и B данная конструкция успешно распарсится компилятором в момент вызова makeString, но если первый аргумент типа double, компилятор не сможет вывести тип std::declval<double>().to_string() и проигнорирует это опеределение.

  2. Вторая функция принимает два параметра, второй из них — это указатель на тот же тип, который вернет вызов Impl::acceptNumber(value), а так, как у нас acceptNumber обьявлен только для int и всех типов, неявно преобразуемых к нему, то попытка подстваить туда struct A или struct B провалится и объявление будет проигнорировано. double же неявно приведется к int, компилятор выведет тип decltype(Impl::acceptNumber(value)) и подстановка успешно сработает.

Запустим, убедимся, что код работает, и попробуем упростить его.

SFINAE и Trailing return type

Альтернатиыной параметрам-пустышкам шаблона и таким же параметрам функции может быть auto для возврящаемого значения. Одно из преимуществ такого решения в том, что ни шаблону, ни методу не добавляются неявные параметры. К слову, это мой любимый вариант использования SFINAE без смс и type_traits в рамках С++17:

// makeString.hpp #pragma once #include <string>  // (3) template <typename Object> auto makeString(const Object &object) -> decltype(object.to_string()) {     return object.to_string(); }  // (4) template <typename Numeric> auto makeString(Numeric value) -> decltype(std::to_string(value)) {     return std::to_string(value); }

В примере выше auto заставит компилятор выводить тип возвращаемого значения, а подсказка вида -> decltype(...)не даст ему этого сделать если:

  1. Не удается вычислить тип, который вернет object.to_string();

  2. Не удается вычислить тип, который вернет std::to_string(object);

На данном этапе код можно считать законченным, он читаем, сопровождаем, но его мало. 17 строк кода на одну статью не хватит, а значит, пора переходить расширить задачу еще одним условием: мы будем делать строки не только из объектов и примитивных типов, но и из коллекций.

Пишем makeString() для коллекций

Итак, расширим задачу:

    // ...      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;

Компилятор заботливо напишет нам о том, что no matching function for call to ‘makeString(const std::vector<int>&)’, т.к. оба имеющихся шаблона не прошли подстановку, а значит нужно написать третий.

Определимся с задачей: у нас есть три разных типа: vector, set, double[]. Между ними должно быть что-то общее.

С моей точки зрения, по всем трем можно итерировать. Вооружимся функцией std::begin() и поверхностными знаниями о SFINAE, чтобы дописать в makeString.hpp теперь уже очевидный метод, возвращаемым значением которого будет тот же тип, который вернет вызов makeString для результата разыменования вызова std::begin для его аргумента:

template <typename Iterable> auto makeString(const Iterable& iterable)      -> decltype(makeString(*std::begin(iterable))) { std::string result; for (const auto& i : iterable) { if (!result.empty()) result += ';'; result += makeString(i); }  return result; }

Скомпилируем, запустим, возрадуемся:

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

Если приведенная выше реализация не показалась вам очевидной, не расстраивайтесь: похоже, у вас еще нет соответствующей профдеформации. А если вы видите очевидные ошибки в этой реализации, то не расстраивайтесь, но профдеформация уже есть.

Попытка написать makeString() для строк

Раз уж мы делаем std::string из примитивных типов, пользовательских классов и самых рахных коллекций, почему бы не сделать строку из С-строки или другой строки.

Допишем новую задачу в int main() и попробуем:

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

Скомпилируем, запустим, работает! Но не так, как хотелось бы:

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 72;101;108;108;111;44;32;0119;111;114;108;10033;33;49

Оказывается, что наша строка подходит под makeString(const Iterable& iterable). Кроме того, тип char — целый, функции std::to_string(char) в стандартной библиотеке нет, а поэтому, как мы все наверняка читали в раздере Integer promotions на одном интересном сайте, char «получает повышение» до int и наш код радостно печатает три пачки целых чисел вместо строк.

Type traits — свойства типов и специализация шаблонов по числовому параметру (by Non-type template parameter)

Итак, нам нужно ограничить примерение шаблона makeString(const Iterable& iterable)только теми типами, которые не строки, и дописать еще одну реализацию для строк. Задача получения свойств типа на этапе компиляции уже решалась до нас, и в общем виде она называется «type traits».

Пусть у нас ничто не строка, кроме std::string, std::string_view и char*. Выразим это условие через С++ код:

namespace Impl { template <typename NotString> inline constexpr bool isString  = false; template <> inline constexpr bool isString<std::string>       = true; template <> inline constexpr bool isString<char*>             = true; template <> inline constexpr bool isString<const char*>       = true; template <> inline constexpr bool isString<const char* const> = true; template <> inline constexpr bool isString<std::string_view>  = true; }

Теперь Imlp::isString<T> будет false для всех типов, кроме тех, для которых есть специализация, возвращающая true. Дело за малым: нужно сделать так, чтобы подстановка в makeString(const Iterable& iterable)не проходила для случаев, когда IsString<Iterable> == true.

Вспомнив, что шаблонным параметром может быть не только произвольный тип, но и значение фиксированного типа, например bool, объявим шаблонный класс, который в общем виде будет пустым, а в специализации для true будет иметь нужный нам параметр:

template <bool B, class T = void> struct enable_if; template <class T> struct enable_if<true, T> { using type = T; }; // syntax sugar: 'enable_if_v' is equivalent of 'typename enable_if<B,T>::type' template <bool B, class T> using enable_if_t = typename enable_if<B,T>::type;

Теперь использование шаблонного типа enable_if<true, T>::type возможно, в то время, как enable_if<false, T>::typeне определен, и вызовет ошибку подстановки (что, как нам известно, «is not an error»). Чтобы немного сократить запись, можно определить псевдоним enable_if_t.

Если внимательный читатель заметил, что оформление примера выше отличается от всего остального, обьясню причину: я его не писал и просто скопипастил из соответствующей статьи одного хорошего справочника.

Собираем код в кучу, избавляемся от плагиата, компилируем и запускаем:

// makeString.hpp #pragma once #include <string> #include <type_traits>  namespace Impl { template <typename NotString> inline constexpr bool isString  = false; template <> inline constexpr bool isString<std::string>       = true; template <> inline constexpr bool isString<char*>             = true; template <> inline constexpr bool isString<const char*>       = true; template <> inline constexpr bool isString<const char* const> = true; template <> inline constexpr bool isString<std::string_view>  = true; }  template <typename Object> auto makeString(const Object& object) -> decltype(object.to_string()) {     return object.to_string(); }  template <typename Numeric> auto makeString(Numeric value) -> decltype(std::to_string(value)) {     return std::to_string(value); }  template <typename Iterable> auto makeString(const Iterable& iterable)      -> std::enable_if_t<!Impl::isString<Iterable>,                          decltype(makeString(*std::begin(iterable)))> {     std::string result;     for (const auto &i : iterable)     {         if (!result.empty())             result += ';';         result += makeString(i);     }      return result; }  template <typename String> auto makeString(const String& s)     -> std::enable_if_t<Impl::isString<String>, std::string> {     return std::string(s); } 

Код все еще работает, но изменений к лучшему еще не видно невооруженным глазом: первый строковый литерал всё еще выводится как массив целых.

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 72;101;108;108;111;44;32;0world!!1

Что же ты, ____ [компилятор], делаешь…

Загружаем хорошо обрезанный кусок кода на CppInsight, компилируем и смотрим выхлоп. Делаем выводы:

  • судя по строке #43 вкладки Insight, шаблон auto makeString(Numeric value) раскрылся для типа char

  • а судя по #68, шаблон auto makeString(const Iterable& iterable) раскрылся для литерала "Hello, " который имеет тип char[8].

  • к слову, компилятор заботливо инстанцировал для нас template<>
    inline constexpr const bool isString<char [8]> = false;
    так как мы не предоставили нужной специализации, чтоб объясняет использование Iterable-версии makeString.

Мы уже знаем, про Non-type template parameters, а потому, по мотивам вывода CppInsight добавим еще одну специализацию. Итак:

// makeString.hpp #pragma once #include <string> #include <type_traits>  namespace Impl { template <typename NotString> inline constexpr bool isString  = false; template <> inline constexpr bool isString<std::string>       = true; template <> inline constexpr bool isString<char*>             = true; template <> inline constexpr bool isString<const char*>       = true; template <> inline constexpr bool isString<const char* const> = true; template <> inline constexpr bool isString<std::string_view>  = true;  template <std::size_t N> inline constexpr bool isString<char[N]> = true; }  template <typename Object> auto makeString(const Object &object) -> decltype(object.to_string()) {     return object.to_string(); }  template <typename Numeric> auto makeString(Numeric value) -> decltype(std::to_string(value)) {     return std::to_string(value); }  template <typename Iterable> auto makeString(const Iterable& iterable)      -> std::enable_if_t<!Impl::isString<Iterable>,                          decltype(makeString(*std::begin(iterable)))> {     std::string result;     for (const auto &i : iterable)     {         if (!result.empty())             result += ';';         result += makeString(i);     }      return result; }  template <typename String> auto makeString(const String& s)     -> std::enable_if_t<Impl::isString<String>, std::string> {     return std::string(s); } 

Компилируем, запускаем, теперь работает именно так, как ожидалось:

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

Библиотека type_traits

Код работает, запускается, но вместе с некоторым пониманием, как оно работает может возникнуть желание глянуть, что же еще интерсного есть на cppreference в стандартной библиотеке для type_traits. А есть там, в частности, trait std::is_convertible, что наталкивает на идею избавиться от собственных велосипедов и их поддержки. Положим, что строка — это то, что неявно конвертируется в std::string. А когда окажется, что std::string_view не конвертируется в std::string неявно, то добавим отдельную специализацию для string_view.

namespace Impl {     template <typename MaybeString>      inline constexpr bool isString = std::is_convertible_v<MaybeString, std::string>;      template <>        inline constexpr bool isString<std::string_view> = true;  // 'cause is not implicitly convertible to std::string }

Компилируем, запускаем, и если сил радоваться больше не осталось, то не радуемся. Но если остались, то обращаем внимание на то, что шаблон auto makeString(const String& s) принимает аргумент по константной ссылке и создает копию. Для вызова makeString(std::string("world")); аргументом которого является временный объект, копию можно было бы не создавать, а использовать перемещение.

Универсальные ссылки и std::forward

Исторически сложилось (несколько часов тому назад), что параметром нашего makeString была шаблонная lvalue-ссылка. Но раз у нас параметр шаблонный, то мы можем вспомнить про идеальную передачу и универсальные ссылки в С++: если шаблонный параметр выглядит как T&&, то при инстанцировании он может принимать как lvalue-ссылку (например, std::string& или const string&), так и rvalue-ссылку на временный объект std::string&&.

При этом нужно помнить, что внутри инстанцируемой функции rvalue всегда превращается в один из видов lvalue. Поднобнее можно ознакомиться в статье по ссылке выше, но на данный момент достаточно понимания того, что T&& в обьявлении шаблонного метода превратится в наиболее подходящую ссылку, которую внутри такого метода можно или передать тем же способом используя std::forward, или преобразовать в rvalue с использованием std::move, или использовать как lvalue.

Поиграть со ссылками и идеальной передачей можно так же на CppInsight, пока же вспомним, что std::forward нам идеально подойдет для того, чтобы перенаправить универсальную ссылку дальше в конструктор std::string:

Кусок документации
template<class T> void wrapper(T&& arg)  {     // arg is always lvalue     foo(std::forward<T>(arg)); // Forward as lvalue or as rvalue, depending on T }
  • If a call to wrapper() passes an rvalue std::string, then T is deduced to std::string (not std::string&, const std::string&, or std::string&&), and std::forward ensures that an rvalue reference is passed to foo.

  • If a call to wrapper() passes a const lvalue std::string, then T is deduced to const std::string&, and std::forward ensures that a const lvalue reference is passed to foo.

  • If a call to wrapper() passes a non-const lvalue std::string, then T is deduced to std::string&, and std::forward ensures that a non-const lvalue reference is passed to foo.

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

template <typename String> auto makeString(String&& s)     -> std::enable_if_t<Impl::isString<String>, std::string> {     return std::string(std::forward<String>(s)); }

Как обычно, компилируем, запускаем, но радуемся только после того, как с помощью функции Step Into отладчика зайдем в перемещающий конструктор std::string(std::string&&) во время вызова makeString(std::string("!!1")) и убедимся, что копирования не происходит. У меня не произошло — я доволен.

// main.cpp #include <iostream> #include <vector> #include <set>  #include "makeString.hpp"  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) + "}"; } };  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; }
// makeString.hpp #pragma once #include <string> #include <type_traits>  namespace Impl {     template <typename MaybeString>      inline constexpr bool isString = std::is_convertible_v<MaybeString, std::string>;      template <>        inline constexpr bool isString<std::string_view> = true;  // 'cause is not implicitly convertible to std::string }  template <typename Object> auto makeString(const Object &object) -> decltype(object.to_string()) {     return object.to_string(); }  template <typename Numeric> auto makeString(Numeric value) -> decltype(std::to_string(value)) {     return std::to_string(value); }  template <typename Iterable> auto makeString(const Iterable &iterable)      -> std::enable_if_t<!Impl::isString<Iterable>,                           decltype(makeString(*std::begin(iterable)))> {     std::string result;     for (const auto &i : iterable)     {         if (!result.empty())             result += ';';         result += makeString(i);     }      return result; }  template <typename String> auto makeString(String&& s)     -> std::enable_if_t<Impl::isString<String>, std::string> {     return std::string(std::forward<String>(s)); }

Variadic templates — шаблоны с переменным числом параметров

Менее сознательный автор на данном этапе спросил бы читателя, не написать ли ему теперь про простое практическое применение шаблонов с переменным числом параметров, но для меня очевидно, что заголовочный файл на 48 строк, из которых половина — отступы, и один тестовый метод main() размером в 20 строк кода, на добротную статью «не тянут».

Итак, как насчет вызова makeString("xs: ", xs, "; and float is: ", 3.14f);? Подобные инициативы грозят очередной бессонной ночью с компилятором, а жизнь — коротка, следовательно бессонных ночей с компилятором в ней мало, а потому не следуют отказывать себе в этом удовольствии.

Расширим задачу еще раз:

std::cout << makeString("xs: ", xs, "; and float is: ", 3.14f)            << std::endl;

И придумаем один из путей решения: makeString с несколькими параметрами — это как makeString с одним параметром, но несколько раз. Другими словами, makeString(a, b, c) эквивалентно makeString(a) + makeString(b) + makeString(c);

Рекурсивный подход к variadic templates

Первая из пришедших в голову идей звучит так: makeString(first, rest...) => makeString(first) + makeString(rest...); до тех пор, пока rest не пустой. А когда rest пустой, рекурсию можно остановить возвратом пустой строки.

std::string makeString() {     return std::string(); }  template <typename First, typename... Rest> std::string makeString(First &&first, Rest &&...rest) {     return makeString(std::forward<First>(first))           + makeString(std::forward<Rest>(rest)...); }

Собрали, запустили, упали. К счасть, не под стол, а по исключению segmentation fault. Суровые линуксоиды снова могут воспользоваться CppInsight, а счастливые пользователи VS 2019 смотрят на предупреждения компилятора и уже видят, что:

warning C4717: ‘makeString<A &>’: recursive on all control paths, function will cause runtime stack oveflow

Действительно, вызов makeString(std::forward(first)) для приводит к вызову std::string makeString(First&& first, Rest&& ...rest) с пустым parameter-pack Rest, в котором мы снова вызываем makeString(First&& first, Rest&& ...rest)с пустым Rest. Таким образом, мы получаем бесконечную рекурсию и переполнение стека.

Но если makeString с переменным числом параметров будет состоять из двух фиксированных параметров и остатка переменной длины, то рекурсию можно остановить на makeString с одним параметром, которых у нас уже написана целая пачка. Проверяем:

template <typename First, typename Second, typename... Rest> std::string makeString(First&& first, Second&& second, Rest&&... rest) { return makeString(std::forward<First>(first))         + makeString(std::forward<Second>(second), std::forward<Rest>(rest)...); }

Окинем взглядом весь наш код перед компиляцией исправленного варианта и запуском:

// makeString.hpp #pragma once #include <string> #include <type_traits>  namespace Impl {     template <typename MaybeString>      inline constexpr bool isString = std::is_convertible_v<MaybeString, std::string>;      template <>        inline constexpr bool isString<std::string_view> = true;  // 'cause is not implicitly convertible to std::string }  template <typename Object> auto makeString(const Object &object) -> decltype(object.to_string()) {     return object.to_string(); }  template <typename Numeric> auto makeString(Numeric value) -> decltype(std::to_string(value)) {     return std::to_string(value); }  template <typename Iterable> auto makeString(const Iterable &iterable)      -> std::enable_if_t<!Impl::isString<Iterable>,                          decltype(makeString(*std::begin(iterable)))> {     std::string result;     for (const auto &i : iterable)     {         if (!result.empty())             result += ';';         result += makeString(i);     }      return result; }  template <typename String> auto makeString(String&& s)     -> std::enable_if_t<Impl::isString<String>, std::string> {     return std::string(std::forward<String>(s)); }  template <typename First, typename Second, typename... Rest> std::string makeString(First&& first, Second&& second, Rest&&... rest) {   return makeString(std::forward<First>(first))           + makeString(std::forward<Second>(second), std::forward<Rest>(rest)...); }

Запускаем, радуемся выводу:

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

Подход со сверткой (fold expression) к Variadic templates

Уже неплохо, но можно лучше: мы можем избежать рекурсии там, где можно свернуть «пачку параметров» c использованием одной и тоже же операции. К счатью, мы наверняка читали в каком-то сравочнике, что начиная с 17го стандарта в C++ есть fold expression, который позволяет свернуть пачку параметров переменной длины в одну большую операцию без рекурсии.

Имея унарную операцию result += x[n], где x[n] — это очередной makeString(pack[n]) , не забывая про возмождность рекурсии и горький опыт переполнения стека, выполним свертку для parameter pack с размером больше 1, т.к. parameter pack размером 1 уже обрабатывается имеющимися шаблонами с одним параметром.

template <typename... Pack> auto makeString(Pack&&... pack)  -> std::enable_if_t<(sizeof...(Pack) > 1), std::string> {     return (... += makeString(std::forward<Pack>(pack))); }

Как обычно, окинем наше произведение взгялом перед запуском:

// main.cpp  #include <iostream> #include <vector> #include <set>  #include "makeString.hpp"  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) + "}"; } };  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; }
// makeString.hpp #pragma once #include <string> #include <type_traits>  namespace Impl {     template <typename MaybeString>      inline constexpr bool isString = std::is_convertible_v<MaybeString, std::string>;      template <>        inline constexpr bool isString<std::string_view> = true;  // 'cause is not implicitly convertible to std::string }  template <typename Object> auto makeString(const Object &object) -> decltype(object.to_string()) {     return object.to_string(); }  template <typename Numeric> auto makeString(Numeric value) -> decltype(std::to_string(value)) {     return std::to_string(value); }  template <typename Iterable> auto makeString(const Iterable &iterable)      -> std::enable_if_t<!Impl::isString<Iterable>,                          decltype(makeString(*std::begin(iterable)))> {     std::string result;     for (const auto &i : iterable)     {         if (!result.empty())             result += ';';         result += makeString(i);     }      return result; }  template <typename String> auto makeString(String&& s)     -> std::enable_if_t<Impl::isString<String>, std::string> {     return std::string(std::forward<String>(s)); }  template <typename... Pack> auto makeString(Pack&&... pack) -> std::enable_if_t<(sizeof...(Pack) > 1), std::string> {     return (... += makeString(std::forward<Pack>(pack))); }

Проверяем:

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

Этим кодом я доволен и на этом хотел бы остановиться. Но если бы я-из-будущего пришел сегодня к себе-сейчас, я бы себе сказал упростить код с использованием концептов, т.к. в будущем концепты поддерживаются даже компиляторами прошивок для холодильников.

Концепты и ограничения (constraints) — синтаксический сахар для SFINAE

Если бы я обновил Visual Studio 2019 до версии 16.3, или gсс и libstdcpp до 10-й, я бы смог использовать концепты для SFINAE.

Концепты — это требования к типу шаблонного параметра, которые компилятор проверяет на этапе подстановки аргументов. По сути, это очень похоже на std::enable_if за исключением того, что нам больше не нужно вручную провоцировать ошибки подстановки.

Предлагаю ознакомиться с концептами, как возможностью языка С++ поближе:

template <typename T>  concept IsString = std::is_convertible_v<T, std::string>                  || std::is_same_v<T, std::string_view>;  template <IsString String> std::string makeString(String&& s) {     return std::string(std::forward<String>(s)); }

Итак, здесь с помощью средств языка задан концепт IsString, которому удовлетворяют те типы, для которых выполняется булево условие is_convertible_v<T, std::string>
|| is_same_v<T, std::string_view>
, где is_convertible_v и is_same_v — это обычные типы из библиотеки type_traits. Далее у нас есть makeString, шаблонный параметр которого не какой-нибудь любой typename, а только тот тип, который удовлетворяет условию IsString.

Кроме того, концепты могут накладывать требование на типы. Пусть концепт HasStdConversion<T> будет проверять, что код T a; std::to_string(a); успешно скомпилируется:

template <typename T> concept HasStdConversion = requires (T number) { std::to_string(number); };

По-моему, стало интереснее. Но мы можем пойти дальше и наложить с помощью концепта требование к типу результата вызова функции для объекта, да простят меня лингвисты за 4 существительных подряд. Для применения требования к типу безо всяких decltype() удобно использовать другие концепты, в том числе, из стандартной библиотеки концептов.

Пусть HasToString будет концептом, который проверяет возможность вызова object.to_string() для своего аргумента и требует, чтобы результат этого вызова удовлетворял концепту std::is_convertible<T, std::string>:

#include <comcepts> // standard library concepts template <typename T>  concept HasToString = requires (const T& object)  {      { object.to_string() } -> std::convertible_to<std::string>;  };  template <HasToString Object> std::string makeString(const Object& object) {     return object.to_string(); }

Заметим, что первый аргумент шаблона концепта всегда подставляется неявно. Это особенности реализации концептов в ядре С++, это просто нужно знать, но благодаря этом их использование выглядит настолько лаконичным, как в наших объявлениях makeString.

Но что, если мы хотим, чтобы один параметр шаблона удовлетворял сразу двум концептам, например, был и контейнером, и не строкой? Очевидно, что мы можем сделать пару концептов контейнер-строка и контейнер-но-не-строка, но заниматься подобной комбинаторикой нет нужды, т.к. у нас есть возможность объявлять требования не только для концепта, но и для конкретного шаблона.

Пусть у нас будут концепты IsContainer, по которому можно итерироваться, и IsString, которому удовлетворяют только строки. Тогда в функции makeString для контейнеров мы можем наложить сразу два требования на её параметр:

template <typename T> concept IsContainer = requires (const T& container) { std::begin(container); };  template <typename T>  concept IsString = std::is_convertible_v<T, std::string>                  || std::is_same_v<T, std::string_view>;  template <typename Container> requires (IsContainer<Container> && !IsString<Container>) std::string makeString(const Container& iterable) {     std::string result;     for (const auto &i : iterable)     {         if (!result.empty())             result += ';';         result += makeString(i);     }      return result; }

В этом шаблоне makeString, typename Container — это тип, на который наложены ограничения IsConainer и not IsString.

Остался шаблон с переменным числом параметров, и по-моему, требование для него получается тривиальным:

template <typename... Pack> requires (sizeof...(Pack) > 1) std::string makeString(Pack&&... pack) {     return (... += makeString(std::forward<Pack>(pack))); }

Стало ли с концептами лучше?

Для ответа на этот вопрос добавим немного не компилирующегося кода:

#include <map> //... int main() { // ...     std::map<int, int> keys = { {1,2}, { 3,4} };     makeString(keys); }

И оценим вывод компилятора:

[build] In file included from ../main.cpp:6: [build] ../makeString.hpp: In instantiation of ‘std::string makeString(const Container&) [with Container = std::map<int, int>; std::string = std::__cxx11::basic_string<char>]’: [build] ../main.cpp:26:20:   required from here [build] ../makeString.hpp:49:29: error: no matching function for call to ‘makeString(const std::pair<const int, int>&)’ [build]    49 |         result += makeString(i); [build]       |                   ~~~~~~~~~~^~~ [build] ../makeString.hpp:29:13: note: candidate: ‘std::string makeString(const Object&) [with Object = std::pair<const int, int>; std::string = std::__cxx11::basic_string<char>]’ [build]    29 | std::string makeString(const Object& object) [build]       |             ^~~~~~~~~~ [build] ../makeString.hpp:29:13: note: constraints not satisfied [build] ../makeString.hpp:35:13: note: candidate: ‘std::string makeString(Numeric) [with Numeric = std::pair<const int, int>; std::string = std::__cxx11::basic_string<char>]’ [build]    35 | std::string makeString(Numeric value) [build]       |             ^~~~~~~~~~ [build] ../makeString.hpp:35:13: note: constraints not satisfied [build] ../makeString.hpp:42:13: note: candidate: ‘std::string makeString(const Container&) [with Container = std::pair<const int, int>; std::string = std::__cxx11::basic_string<char>]’ [build]    42 | std::string makeString(const Container& iterable) [build]       |             ^~~~~~~~~~ [build] ../makeString.hpp:42:13: note: constraints not satisfied [build] ninja: build stopped: subcommand failed. [build] Build finished with exit code 1

В переводе на русский язык, во время инстанцирования makeString для контейнеров в строке 26 моего испорченного кода, не нашлось makeString для pair<int, int>, т.к. ни один из кандидатов не подошёл по требованиям.

Для сравнения, заменю makeString.hpp на версию без Concepts
build] ../main.cpp: In function ‘int main()’: [build] ../main.cpp:26:20: error: no matching function for call to ‘makeString(std::map<int, int>&)’ [build]    26 |     makeString(keys); [build]       |                    ^ [build] In file included from ../main.cpp:6: [build] ../makeString.hpp:16:6: note: candidate: ‘template<class Object> decltype (object.to_string()) makeString(const Object&)’ [build]    16 | auto makeString(const Object &object) -> decltype(object.to_string()) [build]       |      ^~~~~~~~~~ [build] ../makeString.hpp:16:6: note:   template argument deduction/substitution failed: [build] ../makeString.hpp: In substitution of ‘template<class Object> decltype (object.to_string()) makeString(const Object&) [with Object = std::map<int, int>]’: [build] ../main.cpp:26:20:   required from here [build] ../makeString.hpp:16:58: error: ‘const class std::map<int, int>’ has no member named ‘to_string’ [build]    16 | auto makeString(const Object &object) -> decltype(object.to_string()) [build]       |                                                   ~~~~~~~^~~~~~~~~ [build] ../makeString.hpp:22:6: note: candidate: ‘template<class Numeric> decltype (std::__cxx11::to_string(value)) makeString(Numeric)’ [build]    22 | auto makeString(Numeric value) -> decltype(std::to_string(value)) [build]       |      ^~~~~~~~~~ [build] ../makeString.hpp:22:6: note:   template argument deduction/substitution failed: [build] ../makeString.hpp: In substitution of ‘template<class Numeric> decltype (std::__cxx11::to_string(value)) makeString(Numeric) [with Numeric = std::map<int, int>]’: [build] ../main.cpp:26:20:   required from here [build] ../makeString.hpp:22:58: error: no matching function for call to ‘to_string(std::map<int, int>&)’ [build]    22 | auto makeString(Numeric value) -> decltype(std::to_string(value)) [build]       |                                            ~~~~~~~~~~~~~~^~~~~~~ [build] In file included from /usr/include/c++/10/string:55, [build]                  from /usr/include/c++/10/bits/locale_classes.h:40, [build]                  from /usr/include/c++/10/bits/ios_base.h:41, [build]                  from /usr/include/c++/10/ios:42, [build]                  from /usr/include/c++/10/ostream:38, [build]                  from /usr/include/c++/10/iostream:39, [build]                  from ../main.cpp:1: [build] /usr/include/c++/10/bits/basic_string.h:6597:3: note: candidate: ‘std::string std::__cxx11::to_string(int)’ [build]  6597 |   to_string(int __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6597:17: note:   no known conversion for argument 1 from ‘std::map<int, int>’ to ‘int’ [build]  6597 |   to_string(int __val) [build]       |             ~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6608:3: note: candidate: ‘std::string std::__cxx11::to_string(unsigned int)’ [build]  6608 |   to_string(unsigned __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6608:22: note:   no known conversion for argument 1 from ‘std::map<int, int>’ to ‘unsigned int’ [build]  6608 |   to_string(unsigned __val) [build]       |             ~~~~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6616:3: note: candidate: ‘std::string std::__cxx11::to_string(long int)’ [build]  6616 |   to_string(long __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6616:18: note:   no known conversion for argument 1 from ‘std::map<int, int>’ to ‘long int’ [build]  6616 |   to_string(long __val) [build]       |             ~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6627:3: note: candidate: ‘std::string std::__cxx11::to_string(long unsigned int)’ [build]  6627 |   to_string(unsigned long __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6627:27: note:   no known conversion for argument 1 from ‘std::map<int, int>’ to ‘long unsigned int’ [build]  6627 |   to_string(unsigned long __val) [build]       |             ~~~~~~~~~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6635:3: note: candidate: ‘std::string std::__cxx11::to_string(long long int)’ [build]  6635 |   to_string(long long __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6635:23: note:   no known conversion for argument 1 from ‘std::map<int, int>’ to ‘long long int’ [build]  6635 |   to_string(long long __val) [build]       |             ~~~~~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6647:3: note: candidate: ‘std::string std::__cxx11::to_string(long long unsigned int)’ [build]  6647 |   to_string(unsigned long long __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6647:32: note:   no known conversion for argument 1 from ‘std::map<int, int>’ to ‘long long unsigned int’ [build]  6647 |   to_string(unsigned long long __val) [build]       |             ~~~~~~~~~~~~~~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6658:3: note: candidate: ‘std::string std::__cxx11::to_string(float)’ [build]  6658 |   to_string(float __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6658:19: note:   no known conversion for argument 1 from ‘std::map<int, int>’ to ‘float’ [build]  6658 |   to_string(float __val) [build]       |             ~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6667:3: note: candidate: ‘std::string std::__cxx11::to_string(double)’ [build]  6667 |   to_string(double __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6667:20: note:   no known conversion for argument 1 from ‘std::map<int, int>’ to ‘double’ [build]  6667 |   to_string(double __val) [build]       |             ~~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6676:3: note: candidate: ‘std::string std::__cxx11::to_string(long double)’ [build]  6676 |   to_string(long double __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6676:25: note:   no known conversion for argument 1 from ‘std::map<int, int>’ to ‘long double’ [build]  6676 |   to_string(long double __val) [build]       |             ~~~~~~~~~~~~^~~~~ [build] In file included from ../main.cpp:6: [build] ../makeString.hpp:28:6: note: candidate: ‘template<class Iterable> std::enable_if_t<(! isString<Iterable>), decltype (makeString((* std::begin(iterable))))> makeString(const Iterable&)’ [build]    28 | auto makeString(const Iterable &iterable) [build]       |      ^~~~~~~~~~ [build] ../makeString.hpp:28:6: note:   template argument deduction/substitution failed: [build] ../makeString.hpp: In substitution of ‘template<class Iterable> std::enable_if_t<(! isString<Iterable>), decltype (makeString((* std::begin(iterable))))> makeString(const Iterable&) [with Iterable = std::map<int, int>]’: [build] ../main.cpp:26:20:   required from here [build] ../makeString.hpp:30:44: error: no matching function for call to ‘makeString(const std::pair<const int, int>&)’ [build]    30 |                         decltype(makeString(*std::begin(iterable)))> [build]       |                                  ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~ [build] ../makeString.hpp:16:6: note: candidate: ‘template<class Object> decltype (object.to_string()) makeString(const Object&)’ [build]    16 | auto makeString(const Object &object) -> decltype(object.to_string()) [build]       |      ^~~~~~~~~~ [build] ../makeString.hpp:16:6: note:   template argument deduction/substitution failed: [build] ../makeString.hpp: In substitution of ‘template<class Object> decltype (object.to_string()) makeString(const Object&) [with Object = std::pair<const int, int>]’: [build] ../makeString.hpp:30:44:   required by substitution of ‘template<class Iterable> std::enable_if_t<(! isString<Iterable>), decltype (makeString((* std::begin(iterable))))> makeString(const Iterable&) [with Iterable = std::map<int, int>]’ [build] ../main.cpp:26:20:   required from here [build] ../makeString.hpp:16:58: error: ‘const struct std::pair<const int, int>’ has no member named ‘to_string’ [build]    16 | auto makeString(const Object &object) -> decltype(object.to_string()) [build]       |                                                   ~~~~~~~^~~~~~~~~ [build] ../makeString.hpp: In substitution of ‘template<class Iterable> std::enable_if_t<(! isString<Iterable>), decltype (makeString((* std::begin(iterable))))> makeString(const Iterable&) [with Iterable = std::map<int, int>]’: [build] ../main.cpp:26:20:   required from here [build] ../makeString.hpp:22:6: note: candidate: ‘template<class Numeric> decltype (std::__cxx11::to_string(value)) makeString(Numeric)’ [build]    22 | auto makeString(Numeric value) -> decltype(std::to_string(value)) [build]       |      ^~~~~~~~~~ [build] ../makeString.hpp:22:6: note:   template argument deduction/substitution failed: [build] ../makeString.hpp: In substitution of ‘template<class Numeric> decltype (std::__cxx11::to_string(value)) makeString(Numeric) [with Numeric = std::pair<const int, int>]’: [build] ../makeString.hpp:30:44:   required by substitution of ‘template<class Iterable> std::enable_if_t<(! isString<Iterable>), decltype (makeString((* std::begin(iterable))))> makeString(const Iterable&) [with Iterable = std::map<int, int>]’ [build] ../main.cpp:26:20:   required from here [build] ../makeString.hpp:22:58: error: no matching function for call to ‘to_string(std::pair<const int, int>&)’ [build]    22 | auto makeString(Numeric value) -> decltype(std::to_string(value)) [build]       |                                            ~~~~~~~~~~~~~~^~~~~~~ [build] In file included from /usr/include/c++/10/string:55, [build]                  from /usr/include/c++/10/bits/locale_classes.h:40, [build]                  from /usr/include/c++/10/bits/ios_base.h:41, [build]                  from /usr/include/c++/10/ios:42, [build]                  from /usr/include/c++/10/ostream:38, [build]                  from /usr/include/c++/10/iostream:39, [build]                  from ../main.cpp:1: [build] /usr/include/c++/10/bits/basic_string.h:6597:3: note: candidate: ‘std::string std::__cxx11::to_string(int)’ [build]  6597 |   to_string(int __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6597:17: note:   no known conversion for argument 1 from ‘std::pair<const int, int>’ to ‘int’ [build]  6597 |   to_string(int __val) [build]       |             ~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6608:3: note: candidate: ‘std::string std::__cxx11::to_string(unsigned int)’ [build]  6608 |   to_string(unsigned __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6608:22: note:   no known conversion for argument 1 from ‘std::pair<const int, int>’ to ‘unsigned int’ [build]  6608 |   to_string(unsigned __val) [build]       |             ~~~~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6616:3: note: candidate: ‘std::string std::__cxx11::to_string(long int)’ [build]  6616 |   to_string(long __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6616:18: note:   no known conversion for argument 1 from ‘std::pair<const int, int>’ to ‘long int’ [build]  6616 |   to_string(long __val) [build]       |             ~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6627:3: note: candidate: ‘std::string std::__cxx11::to_string(long unsigned int)’ [build]  6627 |   to_string(unsigned long __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6627:27: note:   no known conversion for argument 1 from ‘std::pair<const int, int>’ to ‘long unsigned int’ [build]  6627 |   to_string(unsigned long __val) [build]       |             ~~~~~~~~~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6635:3: note: candidate: ‘std::string std::__cxx11::to_string(long long int)’ [build]  6635 |   to_string(long long __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6635:23: note:   no known conversion for argument 1 from ‘std::pair<const int, int>’ to ‘long long int’ [build]  6635 |   to_string(long long __val) [build]       |             ~~~~~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6647:3: note: candidate: ‘std::string std::__cxx11::to_string(long long unsigned int)’ [build]  6647 |   to_string(unsigned long long __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6647:32: note:   no known conversion for argument 1 from ‘std::pair<const int, int>’ to ‘long long unsigned int’ [build]  6647 |   to_string(unsigned long long __val) [build]       |             ~~~~~~~~~~~~~~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6658:3: note: candidate: ‘std::string std::__cxx11::to_string(float)’ [build]  6658 |   to_string(float __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6658:19: note:   no known conversion for argument 1 from ‘std::pair<const int, int>’ to ‘float’ [build]  6658 |   to_string(float __val) [build]       |             ~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6667:3: note: candidate: ‘std::string std::__cxx11::to_string(double)’ [build]  6667 |   to_string(double __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6667:20: note:   no known conversion for argument 1 from ‘std::pair<const int, int>’ to ‘double’ [build]  6667 |   to_string(double __val) [build]       |             ~~~~~~~^~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6676:3: note: candidate: ‘std::string std::__cxx11::to_string(long double)’ [build]  6676 |   to_string(long double __val) [build]       |   ^~~~~~~~~ [build] /usr/include/c++/10/bits/basic_string.h:6676:25: note:   no known conversion for argument 1 from ‘std::pair<const int, int>’ to ‘long double’ [build]  6676 |   to_string(long double __val) [build]       |             ~~~~~~~~~~~~^~~~~ [build] In file included from ../main.cpp:6: [build] ../makeString.hpp:44:6: note: candidate: ‘template<class String> std::enable_if_t<isString<String>, std::__cxx11::basic_string<char> > makeString(String&&)’ [build]    44 | auto makeString(String&& s) [build]       |      ^~~~~~~~~~ [build] ../makeString.hpp:44:6: note:   template argument deduction/substitution failed: [build] In file included from /usr/include/c++/10/bits/move.h:57, [build]                  from /usr/include/c++/10/bits/nested_exception.h:40, [build]                  from /usr/include/c++/10/exception:148, [build]                  from /usr/include/c++/10/ios:39, [build]                  from /usr/include/c++/10/ostream:38, [build]                  from /usr/include/c++/10/iostream:39, [build]                  from ../main.cpp:1: [build] /usr/include/c++/10/type_traits: In substitution of ‘template<bool _Cond, class _Tp> using enable_if_t = typename std::enable_if::type [with bool _Cond = false; _Tp = std::__cxx11::basic_string<char>]’: [build] ../makeString.hpp:44:6:   required by substitution of ‘template<class String> std::enable_if_t<isString<String>, std::__cxx11::basic_string<char> > makeString(String&&) [with String = std::map<int, int>&]’ [build] ../main.cpp:26:20:   required from here [build] /usr/include/c++/10/type_traits:2554:11: error: no type named ‘type’ in ‘struct std::enable_if<false, std::__cxx11::basic_string<char> >’ [build]  2554 |     using enable_if_t = typename enable_if<_Cond, _Tp>::type; [build]       |           ^~~~~~~~~~~ [build] In file included from ../main.cpp:6: [build] ../makeString.hpp:51:6: note: candidate: ‘template<class ... Pack> std::enable_if_t<(sizeof... (Pack) > 1), std::__cxx11::basic_string<char> > makeString(Pack&& ...)’ [build]    51 | auto makeString(Pack&&... pack) -> std::enable_if_t<(sizeof...(Pack) > 1), std::string> [build]       |      ^~~~~~~~~~ [build] ../makeString.hpp:51:6: note:   template argument deduction/substitution failed: [build] ninja: build stopped: subcommand failed. [build] Build finished with exit code 1

Итоги

Я надеюсь, что в результате этого небольшого но интересного путешествия по справочникам С++, были приобретены и закреплены практические навыки написания полезного и относительно читаемого шаблонного кода. Кроме того, теперь у читателя есть список тем и ссылок для дальнейшего изучения, а у меня — обновлен gcc.

Предлагаю причесать код еще раз, посмотреть на него, и оценить результат:

// main.cpp #include <iostream> #include <vector> #include <set>  #include "makeString.hpp"  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) + "}"; } };  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; }
// makeString.hpp #pragma once #include <string> #include <type_traits> #include <concepts>  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(const Object& object) {     return object.to_string(); }  template <Impl::HasStdConversion Numeric> std::string makeString(Numeric value) {     return std::to_string(value); }  template <typename Container> requires (Impl::IsContainer<Container> && !Impl::IsString<Container>) std::string makeString(const Container& iterable) {     std::string result;     for (const auto &i : iterable)     {         if (!result.empty())             result += ';';         result += makeString(i);     }      return result; }  template <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))); }
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


ссылка на оригинал статьи https://habr.com/ru/post/645321/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *