Використання автоматичних параметрів шаблонів в C++17
В C++17 додали auto
параметри шаблонів. Це значно спростило використання шаблонів. У якості аргументів можна писати, що прийдеться, а компілятор застосовує усі свої дедуктивні методи, щоб з тим розібратися. У цій статті ми розглянемо, як використовувати auto
параметри варіативних шаблонів (variadic templates) на практичному прикладі — серіалізації об’єкту в JSON.
Увага: для компіляції прикладів не забувайте задати опцію —std=c++17 у вашому gcc-8 компіляторі :)
Параметри
Припустимо, що у нас є такий простий клас даних Pdo
:
struct Pdo { bool b; int i; string s; } pdo;
чиї екземпляри нам потрібно писати у форматі JSON. Мова C++ поки що не має механізмів для ітерації по усіх полях класу, тож для стартової позиції нам необхідно власноруч перелічити усі поля як параметри якогось варіативного шаблону. Хай то буде Object
, як в JavaScript:
Object<&Pdo::b, &Pdo::i, &Pdo::s>
Щоб серіалізувати дані, необхідно знати, куди писати, що писати, як писати і які імена дати властивостям в тексті JSON. Куди писати і що писати — нехай це будуть параметри якогось методу, наприклад write
:
Object<&Pdo::b, &Pdo::i, &Pdo::s>::write(pdo, cout);
Імена властивостей здобудемо з __PRETTY_FUNCTION__
(про це див. далі), а як писати — будемо визначати за типом поля. З цими рішеннями фронтальний шаблон отримає такий вигляд:
template<class Class, auto ... Members> struct Object { ostream& write(Class&, ostream&); };
Тут, щоб трохи спростити написання та використання шаблону, ми додали першим параметром клас, з яким працюємо, хоча, якщо є бажання, його можна віднайти за першим параметром шаблону.
Щоб використовувати auto
, слід задати правила дедукції, а це значно простіше робити по кожному параметру окремо, ніж по усіх разом. Для цього додаймо новий шаблон Property
. А щоб працювати з усіма Property
зі списку — ще один шаблон Properties
:
template<auto Member> struct Property; template<class Class, typename ... List> struct Properties;
Початковий пакунок параметрів auto ... Members
перепакуємо в Property<Members> ...
Таким чином, фронтальний шаблон набуває такого вигляду:
template<class Class, auto ... Members> struct Object : Properties<Class, Property<Members> ...> { static ostream& write(const Class& obj, ostream& out) { out << "{"; Properties<Class, Property<Members> ...>::write(obj, out); out << "}"; return out; } };
Стає зрозуміло, якою має бути реалізація write
— запис фігурних дужок та усіх властивостей в порядку їх слідування. Для Properties
напишемо простий ітератор по пакунку параметрів шаблону.
Спеціалізація для першого чи єдиного параметру:
template<class Class, typename First> struct Properties<Class, First> { static inline ostream& write(const Class& obj, ostream& out) { return First::write(obj, out); } };
Спеціалізація для усіх наступних параметрів:
template<class Class, typename First, typename ... List> struct Properties<Class, First, List...> { static inline ostream& write(const Class& obj, ostream& out) { Properties<Class, First>::write(obj, out); out << ","; Properties<Class, List...>::write(obj, out); return out; } };
Із нудними тривіальними речами покінчено, переходимо до цікавих.
Спеціалізації
Щоб компілятор міг застосувати дедукцію, необхідно надати спеціалізації шаблону. У нашому випадку дедукція починається з шаблону Property
. Однак безпосередня спеціалізація від параметру auto
не працює:
template<class Class, typename T> struct Property<T Class::*Member> { }; //error: template argument 1 is invalid
Тож використаємо проміжний шаблон Value
, параметром якого буде тип Type
:
template<typename Type> struct Value;
Загальна спеціалізація для полів будь-яких типів виглядатиме так:
template<class Class, typename T> struct Value<T Class::*> { using class_type = Class; static inline ostream& write(const T& value, ostream& out) { return out << value; } };
У цій спеціалізації шаблону ми говоримо, що параметр Type
шаблону Value
може бути вказівником на поле типу T
у класі Class
. Цей клас ми також зберігаємо у class_type
— він нам знадобиться пізніше. А для запису значення поля використаємо звичний оператор operator<<
. Для числових типів це дасть результат такий, як має бути. Проте для значення типу bool
в JSON слід писати як true
чи false
, в той час як стандартний конвертер пише 1.
То ж зробимо окрему спеціалізацію для bool
:
template<class Class> struct Value<bool Class::*> { using class_type = Class; static inline ostream& write(bool value, ostream& out) { return out << (value ? "true" : "false"); } };
А стрічки (string
) слід писати в лапках, тож зробимо спеціалізацію і для string
:
template<class Class> struct Value<string Class::*> { using class_type = Class; static inline ostream& write(const string& value, ostream& out) { out << quoted(value); return out; } };
І для const char*
:
template<class Class> struct Value<const char* Class::*> { using class_type = Class; static inline ostream& write(const char* value, ostream& out) { out << quoted(value); return out; } };
І для char*
— тут ми просто перенаправимо на спеціалізацію для const char*
:
template<class Class> struct Value<char* Class::*> : Value<const char* Class::*> {};
Визначивши що є Value
, повернемось до Property
і напишемо його реалізацію з використанням Value
:
template<auto Member> struct Property : Value<decltype(Member)> { using Class = typename Value<decltype(Member)>::class_type; static inline ostream& write(const Class& obj, ostream& out) { out << quoted((const char*)nameof<Member>()) << "="; return Value<decltype(Member)>::write(obj.*Member,out); } };
В той час як параметр Member
шаблону Property
— це вказівник на поле, в шаблон Value
ми передаємо тип цього вказівника, який отримуємо за допомогою decltype(Member)
. Клас, до якого належить вказівник, ми отримуємо із шаблону Value
, де ми його завбачливо зберегли в class_type
. Використану тут функцію nameof<Member>()
, що має повернути нам ім’я поля класу, зараз і напишемо. Для цього використаємо вбудовану змінну __PRETTY_FUNCTION__
, яка в останніх версіях компілятора має кваліфікатор constexpr
.
template<auto V> constexpr string_view nameof() { constexpr string_view pretty = __PRETTY_FUNCTION__; return pretty; }
Така функція поверне нам стрічку з гарним ім’ям функції-шаблону nameof
:
constexpr std::string_view nameof() [with auto V = &Pdo::i; std::string_view = std::basic_string_view<char>]
Якщо уважно придивитись, то вона містить те, що нам потрібно — &Pdo::i. Треба тільки знайти його у стрічці. Шукати будемо з кінця:
template<auto V> constexpr string_view nameof() { constexpr string_view pretty = __PRETTY_FUNCTION__; constexpr auto end = pretty.rfind(';'); constexpr auto begin = pretty.rfind(':',end) + 1; return pretty.substr(begin, end - begin); }
Цю функцію вже можна використовувати як є, однак слід пам’ятати, що бінарний файл буде містити всі екземпляри __PRETTY_FUNCTION__
в повну їх довжину. Щоб цього уникнути, слід зробити копію часу компіляції з потрібної частини стрічки. Для копіювання із string_view
напишемо новий шаблон, який зберігатиме constexpr
копію, зроблену посимвольно за допомогою index_sequence
. Для його використання потрібно два конструктори: один прийматиме в параметри тільки стрічку, а інший, шаблонний, — стрічку і об’єкт індексатор. Рознесемо ці конструктори по різних класах: basic_cestring
— допоміжний клас з індексатором та cestring
— що його наслідує і має конструктор тільки зі стрічкою.
template<typename CharT, size_t Size> struct basic_cestring { using value_type = CharT; /* конструктор посимвольного копіювання */ template<size_t... I> constexpr basic_cestring(const char* str, index_sequence<I...>) : _data{str[I]...} {} inline constexpr operator const CharT* () const { return _data; } const CharT _data[Size + 1]; };
template<size_t Size> struct cestring : public basic_cestring<char, Size> { using index = make_index_sequence<Size>; constexpr cestring(const char* str) : basic_cestring<char, Size>(str, index{}) {} };
Екземпляр індексатора index{}
, який ми передаємо в basic_cestring
, потрібний лише як засіб передачі типу пакунку параметрів size_t... I
в шаблон конструктора basic_cestring
.
З такими змінами функція nameof
набуде вигляду:
template<auto V> constexpr auto nameof() { constexpr string_view pretty = __PRETTY_FUNCTION__; constexpr auto end = pretty.rfind(']'); constexpr auto begin = pretty.rfind(':',end) + 1; constexpr auto name = pretty.substr(begin, end - begin); constexpr auto length = name.length(); constexpr cestring<length> result { name.data() }; return result; }
З тим, що ми вже написали, ми можемо писати JSON таким викликом:
Object<Pdo, &Pdo::b, &Pdo::i, &Pdo::s>::write(obj, cout);
Однак хотілося б просто cout << obj;
. Для цього перегрузимо operator<<
:
template<class Class> inline ostream& operator<<(ostream& stream, const Class& object) { // і де ж тут взяти Object<Class, ...> ? return stream; }
Щоб operator<<
знав, як серіалізувати наш клас, потрібно асоціювати клас із його моделлю. Зупинимось на простому рішенні — вкладений тип з певним ім’ям. Наприклад так:
struct Pdo { bool b; int i; string s; using Json = Object<Pdo, &Pdo::b, &Pdo::i, &Pdo::s>; };
Також у такій формі він перекриє всі інші шаблони операторів <<
з класами. Щоб цього не трапилось, використаємо шаблон оператора з додатковим параметром-запобіжником, для якого і використаємо вкладений тип Json
. Тоді operator<<
матиме вигляд:
template<class Class, class Json = typename Class::Json> inline ostream& operator<<(ostream& stream, const Class& object) { return Json::write(object, stream); }
Як винагороду ми отримуємо можливість писати вкладені об’єкти:
struct Cdo { Pdo pdo; double val; using Json = Object<Cdo, &Cdo::pdo, &Cdo::val>; } cdo; cout << cdo;
Вектори
Із типів даних JSON залишився не розглянутим масив — []. В межах цієї статті обмежимось лиш гомогенними масивами — векторами. Щоб мати змогу їх писати, спеціалізуємо шаблон Value
для векторів:
template<class Class, typename T> struct Value<vector<T> Class::*> { using class_type = Class; static inline ostream& write(const vector<T>& value, ostream& out) { const char* dlm = ""; out << "["; for(auto & i : value) { out << dlm << i; dlm = ","; } return out << "]"; } };
Така спеціалізація відкрила нам можливість серіалізувати як вектори простих типів так і вектори об’єктів, за умови, що вони мають модель JSON визначену як вкладений тип Json
.
struct Cdo { Pdo pdo; double val; vector<int> vect; vector<Pdo> array; using Json = Object<Cdo, &Cdo::pdo, &Cdo::val, &Cdo::vect, &Cdo::array>; };
З тим, що ми з вами тут накодували, ми можемо писати об’єкт як JSON, проте JSON-текст — це не обов’язково об’єкт. Це може бути масив, стрічка чи навіть скалярна величина. Щоб підтримати тип значення масив, спеціалізуємо Value
без Class
:
template<typename T> struct Value<vector<T> *> { static inline ostream& write(const vector<T>& value, ostream& out) { const char* dlm = ""; out << "["; for(auto & i : value) { out << dlm << i; dlm = ","; } return out << "]"; } };
та для зручності використання створимо новий шаблон для інтерпретації вектора як JSON-масиву:
template<typename Vector> struct Array { inline ostream& write(ostream& out) const { return Value<Vector>::write(vector_, out); } inline constexpr Array(const Vector& vector) : vector_(vector) {} private: const Vector& vector_; };
З таким шаблоном масив можна писати викликом методу write
:
Array(v).write(cout)
А для того, щоб використати operator<<
, доповнимо цей шаблон внутрішнім типом Json
та статичним методом write
:
template<typename Vector> struct Array { using Json = Array<Vector>; inline ostream& write(ostream& out) const { return Value<Vector>::write(vector_, out); } static inline ostream& write(const Array<Vector>& vector, ostream& out) { return Value<Vector>::write(vector.vector_, out); } inline constexpr Array(const Vector& vector) : vector_(vector) {} private: const Vector& vector_; }; сout << Array(v);
Підсумок
Отже, витративши трішки часу на вивчення шаблонів з auto параметрами та написавши 160 рядків коду, ми створили можливість писати доволі складні JSON-об’єкти за допомогою простих конструкцій:
struct Pdo { bool b; int i; string s; using Json = Object<Pdo, &Pdo::b, &Pdo::i, &Pdo::s>; };
struct Cdo { Pdo pdo; double val; vector<int> vect; vector<Pdo> array; using Json = Object<Cdo, &Cdo::pdo, &Cdo::val, &Cdo::vect, &Cdo::array>; }; Cdo cdo { {false, 15, "Cdo"}, 1.2, {1, 2, 3, 5, 7}, {{true, 17, "cdo.pdo"}} }; cout << cdo;
Також ми розглянули, як робити спеціалізацію шаблонів з auto параметрами для дедукції, простий ітератор по пакунку параметрів, копії стрічок часу компіляції, використання index_sequence
для ітерацій та трюк з __PRETTY_FUNCTION__
для отримання імен ідентифікаторів.
Додаток. Довершений код
Довершений код до цієї статті наведено нижче, а результат виконання можна подивитись тут.
#include <iostream> #include <string> #include <vector> #include <iomanip> using namespace std; template<typename CharT, size_t Size> struct basic_cestring { using value_type = CharT; /* конструктор посимвольного копіювання */ template<size_t... I> constexpr basic_cestring(const char* str, index_sequence<I...>) : _data{str[I]...} {} inline constexpr operator const CharT* () const { return _data; } private: const CharT _data[Size + 1]; }; template<size_t Size> struct cestring : public basic_cestring<char, Size> { using index = make_index_sequence<Size>; constexpr cestring(const char* str) : basic_cestring<char, Size>(str, index{}) {} }; template<auto V> constexpr auto nameof() { constexpr string_view pretty = __PRETTY_FUNCTION__; constexpr auto end = pretty.rfind(']'); constexpr auto begin = pretty.rfind(':',end) + 1; constexpr auto name = pretty.substr(begin, end - begin); constexpr auto length = name.length(); constexpr cestring<length> result { name.data() }; return result; } template<typename Type> struct Value; template<class Class, typename T> struct Value<T Class::*> { using class_type = Class; static inline ostream& write(const T& value, ostream& out) { return out << value; } }; template<class Class> struct Value<bool Class::*> { using class_type = Class; static inline ostream& write(bool value, ostream& out) { return out << (value ? "true" : "false"); } }; template<class Class> struct Value<string Class::*> { using class_type = Class; static inline ostream& write(const string& value, ostream& out) { out << quoted(value); return out; } }; template<class Class> struct Value<const char* Class::*> { using class_type = Class; static inline ostream& write(const char* value, ostream& out) { out << quoted(value); return out; } }; template<class Class> struct Value<char* Class::*> : Value<const char* Class::*> {}; template<typename T> struct Value<vector<T>> { static inline ostream& write(const vector<T>& value, ostream& out) { const char* dlm = ""; out << "["; for(auto & i : value) { out << dlm << i; dlm = ","; } return out << "]"; } }; template<class Class, typename T> struct Value<vector<T> Class::*> { using class_type = Class; static inline ostream& write(const vector<T>& value, ostream& out) { const char* dlm = ""; out << "["; for(auto & i : value) { out << dlm << i; dlm = ","; } return out << "]"; } }; template<auto Member> struct Property : Value<decltype(Member)> { using Class = typename Value<decltype(Member)>::class_type; static inline ostream& write(const Class& obj, ostream& out) { out << quoted((const char*)nameof<Member>()) << "="; return Value<decltype(Member)>::write(obj.*Member,out); } }; template<class Class, typename ... List> struct Properties; template<class Class, typename First> struct Properties<Class, First> { static inline ostream& write(const Class& obj, ostream& out) { First::write(obj, out); return out; } }; template<class Class, typename First, typename ... List> struct Properties<Class, First, List...> { static inline ostream& write(const Class& obj, ostream& out) { Properties<Class, First>::write(obj, out); out << ","; Properties<Class, List...>::write(obj, out); return out; } }; template<class Class, auto ... Members> struct Object : Properties<Class, Property<Members> ...> { static ostream& write(const Class& obj, ostream& out) { out << "{"; Properties<Class, Property<Members> ...>::write(obj, out); out << "}"; return out; } }; template<typename Vector> struct Array { using Json = Array<Vector>; inline ostream& write(ostream& out) const { return Value<Vector>::write(vector_, out); } static inline ostream& write(const Array<Vector>& vector, ostream& out) { return Value<Vector>::write(vector.vector_, out); } inline constexpr Array(const Vector& vector) : vector_(vector) {} private: const Vector& vector_; }; template<class Class, class Json = typename Class::Json> inline ostream& operator<<(ostream& stream, const Class& object) { return Json::write(object, stream); } //---------------------------------------------------------------------------- struct Pdo { bool b; int i; string s; using Json = Object<Pdo, &Pdo::b, &Pdo::i, &Pdo::s>; }; struct Cdo { Pdo pdo; double val; vector<int> vect; vector<Pdo> array; using Json = Object<Cdo, &Cdo::pdo, &Cdo::val, &Cdo::vect, &Cdo::array>; }; int main() { vector<unsigned> v { 13, 17 }; Pdo obj { true, 11, "ss\"s" }; Cdo cdo { {false, 15, "Cdo"}, 1.2, {1, 2, 3, 5, 7}, {{true, 17, "cdo.pdo"}} }; cout << Array(v) << endl; cout << Array(cdo.array) << endl; Object<Pdo, &Pdo::b, &Pdo::i, &Pdo::s>::write(obj, cout) << endl; cout << obj << endl; cout << cdo << endl; return 0; }