После недавней статьи о шаблонах С++ для начинающих осталось жгучее желание показать что-нибудь похожее, но на практическом примере, да так, чтобы и порог входа был не высоким, и чтобы скучно не было. А так как в голове крутится задача перевода чего бы то ни было в строку, то этим и предлагаю заняться всем, кто хочет потрогать компилятор за шаблоны.
Оглавление
Проблема
В имеющемся 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); }
Постараюсь расшифровать приведенные выше руны:
-
Первая функция имеет два шаблонных параметра: тип объекта, и безымяный неиспользуемый параметр того же типа, который вернет вызов
Object::to_string()
для сферического экземпляраObject
в вакууме.std::declval<Object>
в данном случае — это замена конструктора по умолчанию, т.к. у типа Object такого конструктора может не быть.Для типов
A
иB
данная конструкция успешно распарсится компилятором в момент вызоваmakeString
, но если первый аргумент типаdouble
, компилятор не сможет вывести типstd::declval<double>().to_string()
и проигнорирует это опеределение. -
Вторая функция принимает два параметра, второй из них — это указатель на тот же тип, который вернет вызов
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(...)
не даст ему этого сделать если:
-
Не удается вычислить тип, который вернет
object.to_string()
; -
Не удается вычислить тип, который вернет
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 rvaluestd::string
, thenT
is deduced tostd::string
(notstd::string&
,const std::string&
, orstd::string&&
), andstd::forward
ensures that an rvalue reference is passed tofoo
. -
If a call to
wrapper()
passes a const lvaluestd::string
, thenT
is deduced toconst std::string&
, andstd::forward
ensures that a const lvalue reference is passed tofoo
. -
If a call to
wrapper()
passes a non-const lvaluestd::string
, thenT
is deduced tostd::string&
, andstd::forward
ensures that a non-const lvalue reference is passed tofoo
.
В результате чтения тонны документации и нескольких статей, приходим к идеальной передаче параметра в нашей специализации для строк:
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/