Використання автоматичних параметрів шаблонів в 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;
}
Похожие статьи:
В рубрике DOU Проектор все желающие могут презентовать свой продукт (как стартап, так и ламповый pet-проект). Если вам есть о чем...
Machine Learning Engineer— це спеціаліст, який розбирається в алгоритмах машинного та глибокого навчання і здатен натренувати...
На 4-му Diia Summit офіційно запустили спецрежим для ІТ-галузі Дія City. Влада очікує, що частка ІТ у ВВП держави зросте з 4%...
Початок 2020 року — традиційний фотоогляд святкування Нового року в українських ІТ-компаніях. AB Games Новорічний...
У листопаді Михайло Федоров оголосив про наміри створити в Україні платформу фрилансерів на кшталт Upwork....
Яндекс.Метрика