После недавней статьи о шаблонах С++ для начинающих осталось жгучее желание показать что-нибудь похожее, но на практическом примере, да так, чтобы и порог входа был не высоким, и чтобы скучно не было. А так как в голове крутится задача перевода чего бы то ни было в строку, то этим и предлагаю заняться всем, кто хочет потрогать компилятор за шаблоны.
Оглавление
Концепты и ограничения (constraints) - альтернатива классическому SFINAE
Отличие концептов от классического SFINAE и Partial ordering of constraints
Проблема
В имеющемся 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
, подходят под параметр шаблона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
.
Если внимательный читатель заметил, что оформление примера выше отличается от всего остального, объясню причину: я его не писал, а просто сплагиатил реализацию std::enable_if
из соответствующей статьи одного хорошего справочника.
Собираем код в кучу, избавляемся от плагиата, компилируем и запускаем:
// 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)
принимает аргумент по константной ссылке и всегда возвращает её копию. Мы можем лучше.
Универсальные ссылки и std::forward
В вызовах вроде makeString(std::string("world"));
мы могли бы перемещать временный объект в возвращаемое значение, если бы на момент написания кода makeString
для строк мы дочитали C++ Core Guidelines до рекомендации "использовать универсальную ссылку и std::forward
для параметров, которые не используются вызываемой функцией, а передаются дальше по цепочке вызовов".
Подробнее об этом написана целая статья про идеальную передачу и универсальные ссылки в С++: если шаблонный параметр выглядит как T&&
, то при инстанцировании он может принимать как lvalue-ссылку (например, std::string&
или const string&
), так и rvalue-ссылку на временный объект std::string&&
.
При этом нужно помнить, что внутри инстанцируемой функции rvalue всегда превращается в один из видов lvalue, со ссылкой или без, и дальше его можно или передать тем же способом используя std::forward, или преобразовать в rvalue с использованием std::move
, или использовать как lvalue.
Другими словами, если параметр Type
шаблонный, следующая конструкция передаст параметр дальше по цепочке вызовов, не требуя ни константность, ни "rvalue-ness", при этом передавая и "const-ness" и "rvalue-ness" дальше в неизменном виде, если они были у параметра:
template <typename Type> void wrapper(Type&& value)
{
f(std::forward<Type>(value));
}
int i = 0;
const int ci = 1;
int getInt();
wrapper(i); // call to f<int&>(i)
wrapper(ci); // call to f<const int&>(ci)
wrapper(getInt()); // call to f<int&&>(temporary_int)
Кусок документации к std::forward
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));
}
Как обычно, компилируем, запускаем, но на слово автору статьи не верим и смостоятельно в отладчике проверяем, что во время вызова makeString(std::string("!!1"))
для возврата временного объекта из функции стал вызываться перемещающий конструктор std::string
.
// 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
– это обычные constexpr bool
trait-ы из библиотеки 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)));
}
Как обычно, компилируем, запускаем, и радуемся, что код работает, как задумано, потому что у нас нет контейнеров, которые самостоятельно переводят себя в строку, тем самым удовлетворяя одновременно и концепту IsContainer
, и HasToString
. Спасибо, @sergegers, за отличный тест-кейс.
Отличие концептов от классического SFINAE и Partial ordering of constraints
А что, если контейнер с собственным to_string всё-таки есть?
struct C
{
std::string m_string;
auto begin() const { return std::begin(m_string); }
auto begin() { return std::begin(m_string); }
auto end() const { return std::end(m_string); }
auto end() { return std::end(m_string); }
std::string to_string() const { return "C{\"" + m_string + "\"}"; }
};
// ...
std::cout << makeString( C { "a container with its own to_string()" } )
<< std::endl;
В таком виде, ни makeString
со SFINAE, ни вариант с концептами не скомпилируется из-за неоднозначности, т.к. класс C
одновременно удовлетворяет и требованию IsContainer
и требованию HasToString
, при этом оба концепта имеют одинаковый приоритет. Кратко о приоритете применения ограничений к шаблонам (Partial ordering of constraints) можно прочитать сами-знаете-где, а вот подробнее и понятнее рассказал Andrzej's в статье Ordering by constraints.
Если упрощённо, то применяется наиболее ограниченный constraint-ами вызов из всех подходящих. При этом степень ограниченности тем больше, чем больше requirement-ов или концептов применено. Подробнее о логике подсчета можно написать хорошую, добротную, статью (в чем я советую убедиться всем желающим по ссылке выше), но в двух словах: ограничение P более ограничено, чем ограничение Q только в том случае, если множество объектов Q включает в себя объекты P, но не наоборот. Во время этого анализа компилятор не может "заглядывать" внутрь выражений типа bool
, поэтому классические constexpr
trait-ы из C++17 могут не привести к ожидаемому результату.
Для того, чтобы порядок применения заработал так, как нам нужно, перепишем имеющиеся концепты так, чтобы они не содержали друг друга и отличались по степени ограничивания:
namespace Impl
{
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 = !Impl::HasToString<T> && requires(const T& container)
{
std::begin(container);
};
template <typename T>
concept IsString = IsContainer<T> && std::constructible_from<std::string, T>;
}
Итого, имеем:
HasToString
- это класс без ограничений, у которого есть методto_string()
, возвращающий то, что неявно приводится кstd::string
.HasStdConversion
- это класс без ограничений, такой, что вызовstd::to_string(number);
для экземпляра этого классаnumber
будет успешно скомпилированIsContainer
- это!HasToString
, для которого дополнительно вводится ограничение на компилируемость выраженияstd::begin(container)
IsString
- этоIsContainer
для которого вводится ограничение "std::string
можно явно сконструировать изT
". Обратим внимание, что в данном случаеstd::constructible_from
- это тоже концепт, что позволяет компилятору однозначно понять, чтоIsString
более ограничивающий, чемIsContainer
.
Теперь строковый литерал "hello"
будет удовлетворять концепту IsString
(т.е. sContainer && constructible_from<std::string, T>
), std::vector<char> {'a', 'b'}
будет удовлетворять концепту IsContainer
, но не IsString
, а обертка вокруг std::vector
со своим собственным to_string
перестанет быть IsContainer
, потому что IsContainer
требует !Impl::HasToString
.
Обновим реализации makeString
, не забывая про perfect forwarding:
// makeString.hpp
#pragma once
#include <concepts>
#include <iostream>
#include <set>
#include <string>
#include <type_traits>
#include <vector>
namespace Impl
{
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 = !Impl::HasToString<T> && requires(const T& container)
{
std::begin(container);
};
template <typename T>
concept IsString = IsContainer<T> && std::constructible_from<std::string, T>;
} // namespace Impl
template <Impl::HasToString Object>
std::string makeString(Object&& object)
{
return object.to_string();
}
template <Impl::HasStdConversion Numeric>
std::string makeString(Numeric&& value)
{
return std::to_string(std::forward<Numeric>(value));
}
template <Impl::IsString String>
std::string makeString(String&& s)
{
return std::string(std::forward<String>(s));
}
template <Impl::IsContainer Container>
std::string makeString(Container&& iterable)
{
std::string result;
for (auto&& i: std::forward<Container>(iterable))
{
if (!result.empty())
result += ';';
result += makeString(std::forward<decltype(i)>(i));
}
return result;
}
template <typename... 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
C{"a conatiner with its own to_string()"}
Стало ли с концептами лучше?
Для ответа на этот вопрос добавим немного не компилируемого кода:
#include <map>
//...
int main()
{
// ...
std::map<int, int> keys = { {1,2}, { 3,4} };
makeString(keys);
}
И оценим вывод компилятора:
[main] Building folder: vscode-test
[build] Starting build
[proc] Executing command: /usr/bin/cmake --build /home/victor/vscode-test/build --config Debug --target all -j 4 --
[build] [1/2 50% :: 5.122] Building CXX object CMakeFiles/makeString.dir/main.cpp.o
[build] FAILED: CMakeFiles/makeString.dir/main.cpp.o
[build] /usr/bin/x86_64-linux-gnu-g++-10 -g -Wall -Wextra -pedantic -Werror -std=gnu++2a -MD -MT CMakeFiles/makeString.dir/main.cpp.o -MF CMakeFiles/makeString.dir/main.cpp.o.d -o CMakeFiles/makeString.dir/main.cpp.o -c ../main.cpp
[build] In file included from ../main.cpp:8:
[build] ../makeString.hpp: In instantiation of ‘std::string makeString(Container&&) [with Container = std::map<int, int>&; std::string = std::__cxx11::basic_string<char>]’:
[build] ../main.cpp:63:20: required from here
[build] ../makeString.hpp:59:29: error: no matching function for call to ‘makeString(std::pair<const int, int>&)’
[build] 59 | result += makeString(std::forward<decltype(i)>(i));
[build] | ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[build] ../makeString.hpp:34:13: note: candidate: ‘std::string makeString(Object&&) [with Object = std::pair<const int, int>&; std::string = std::__cxx11::basic_string<char>]’
[build] 34 | std::string makeString(Object&& object)
[build] | ^~~~~~~~~~
[build] ../makeString.hpp:34:13: note: constraints not satisfied
[build] ../makeString.hpp:40:13: note: candidate: ‘std::string makeString(Numeric&&) [with Numeric = std::pair<const int, int>&; std::string = std::__cxx11::basic_string<char>]’
[build] 40 | std::string makeString(Numeric&& value)
[build] | ^~~~~~~~~~
[build] ../makeString.hpp:40:13: note: constraints not satisfied
[build] ../makeString.hpp:46:13: note: candidate: ‘std::string makeString(String&&) [with String = std::pair<const int, int>&; std::string = std::__cxx11::basic_string<char>]’
[build] 46 | std::string makeString(String&& s)
[build] | ^~~~~~~~~~
[build] ../makeString.hpp:46:13: note: constraints not satisfied
[build] ../makeString.hpp:52:13: note: candidate: ‘std::string makeString(Container&&) [with Container = std::pair<const int, int>&; std::string = std::__cxx11::basic_string<char>]’
[build] 52 | std::string makeString(Container&& iterable)
[build] | ^~~~~~~~~~
[build] ../makeString.hpp:52:13: note: constraints not satisfied
[build] ninja: build stopped: subcommand failed.
[build] Build finished with exit code 1
В переводе на русский язык, во время инстанцирования makeString
для контейнеров в строке 63 моего испорченного кода, не нашлось 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) + "}"; }
};
struct C
{
std::string m_string;
auto begin() const { return std::begin(m_string); }
auto begin() { return std::begin(m_string); }
auto end() const { return std::end(m_string); }
auto end() { return std::end(m_string); }
std::string to_string() const { return "C{\"" + m_string + "\"}"; }
};
int main()
{
A a;
B b = {1};
const std::vector<int> xs = {1, 2, 3};
const std::set<float> ys = {4, 5, 6};
const double zs[] = {7, 8, 9};
std::cout << "a: " << makeString(a) << "; b: " << makeString(b)
<< "; pi: " << makeString(3.14) << std::endl
<< "xs: " << makeString(xs) << "; ys: " << makeString(ys)
<< "; zs: " << makeString(zs)
<< std::endl;
std::cout << makeString("Hello, ")
<< makeString(std::string_view("world"))
<< makeString(std::string("!!1"))
<< std::endl;
const std::string constHello = "const hello!";
std::cout << makeString(constHello)
<< std::endl;
std::cout << makeString("xs: ", xs, "; and float is: ", 3.14f)
<< std::endl;
std::cout << makeString(C { "a conatiner with its own to_string()" })
<< std::endl;
}
// makeString.hpp
#pragma once
#include <concepts>
#include <iostream>
#include <set>
#include <string>
#include <type_traits>
#include <vector>
namespace Impl
{
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 = !Impl::HasToString<T> && requires(const T& container)
{
std::begin(container);
};
template <typename T>
concept IsString = IsContainer<T> && std::constructible_from<std::string, T>;
} // namespace Impl
template <Impl::HasToString Object>
std::string makeString(Object&& object)
{
return object.to_string();
}
template <Impl::HasStdConversion Numeric>
std::string makeString(Numeric&& value)
{
return std::to_string(std::forward<Numeric>(value));
}
template <Impl::IsString String>
std::string makeString(String&& s)
{
return std::string(std::forward<String>(s));
}
template <Impl::IsContainer Container>
std::string makeString(Container&& iterable)
{
std::string result;
for (auto&& i: std::forward<Container>(iterable))
{
if (!result.empty())
result += ';';
result += makeString(std::forward<decltype(i)>(i));
}
return result;
}
template <typename... 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
C{"a conatiner with its own to_string()"}
UPD: добавил краткое описание приоритетов ограничений и поддержку контейнеров с собственным to_string()
.