Pull to refresh

Literal operator templates for strings

Reading time4 min
Views15K
Стандарт C++11 привнес в язык такую вещь, как пользовательские литералы[1]. Конкретно — дюжину вариантов для определения оператора "", добавляющих небольшой синтаксический сахар, всех, за исключением одного — шаблонного варианта:

template <char...> type operator "" _op();

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

Например:

auto x = 10001000100011001001001010001000_b;

Однако при разработке стандарта было допущено небольшое упущение — шаблонный вариант пользовательского литерала позволяет работать только с числовыми аргументами, несмотря на то, что парсинг их осуществляется посимвольно.
Такое упущение, конечно же, не могло остаться не замеченным, и на этапе согласования стандарта C++14 было предложено решение для строковых аргументов[2]

template <typename CharT, CharT ...String> type operator "" _op();

В скором времени было реализовано в компиляторах GCC[3] и clang (GNU extension). Однако в финальную редакцию стандарта C++14 так и не попало. Впрочем, не будем отчаиваться, есть надежда, что нас обрадует C++17. А пока посмотрим, как можно будет применять новый тип пользовательских литералов.

Определим шаблон мета-строки:

template<char ... Chars>
struct str {
    static constexpr const char value[sizeof...(Chars)+1] = {Chars...,'\0'};
    static constexpr int size = sizeof...(Chars);
};

template<char ... Chars>
constexpr const char str<Chars...>::value[sizeof...(Chars)+1];

Определим наш литерал-генератор мета-строки:

template<typename CharT, CharT ...String>
constexpr str<String...> operator"" _s()
{
    return str<String...>();
}

Создадим шаблон map-like структуры данных, с типами в качестве ключей:

template<class Type, class Key>
struct field {
    using key = Key;
    using type = Type;
    type value;
};

template<class,class,int N=0>
struct field_by_type;

template<class Key, class Type, class ... Tail, int N>
struct field_by_type<Key, std::tuple<field<Type,Key>,Tail...>, N> {
    static constexpr int value = N;
};

template<class Key, class Head, class ... Tail, int N>
struct field_by_type<Key, std::tuple<Head,Tail...>, N> : field_by_type<Key,std::tuple<Tail...>,N+1> {};

template<class ... Fields>
struct record {
    
    using tuple_type = std::tuple<Fields...>;

    template<class Key>
    typename std::tuple_element<field_by_type<Key,tuple_type>::value,tuple_type>::type::type& operator[](Key) {
        return std::get<field_by_type<Key,tuple_type>::value>(data).value;
    }

    template<class Key>
    const typename std::tuple_element<field_by_type<Key,tuple_type>::value,tuple_type>::type::type& operator[](Key) const {
        return std::get<field_by_type<Key,tuple_type>::value>(data).value;
    }
    
    tuple_type data;
};

Так как в качестве типов-ключей мы собираемся использовать мета-строки, добавим немного ввода-вывода:

template<class Type, class Key>
std::ostream& operator<< (std::ostream& os, const field<Type,Key> f){
    os << Key::value << " = " << f.value << "\n";
    return os;
}

template<int I, typename... Ts>
struct print_tuple {
    std::ostream& operator() (std::ostream& os, const std::tuple<Ts...>& t) {
        os << std::get<sizeof...(Ts)-I>(t);
        return print_tuple<I - 1, Ts...>{}(os,t);
    }
};

template<typename... Ts>
struct print_tuple<0, Ts...> {
    std::ostream& operator() (std::ostream& os, const std::tuple<Ts...>& t) {
        return os;
    }
};

template<class ... Fields>
std::ostream& operator<< (std::ostream& os, const record<Fields...>& r) {
    os << "{\n";
    print_tuple<sizeof...(Fields),Fields...>{}(os,r.data);
    os << "}";
    return os;
}

Ну а теперь и сам пример:
using Person = record<
        field<int, decltype("id"_s)>,
        field<std::string, decltype("first_name"_s)>,
        field<std::string, decltype("last_name"_s)>
    >;

int main(){
    Person p;
    p["id"_s] = 10;
    p["first_name"_s] = "John";
    p["last_name"_s] = "Smith";
    std::cout << p << "\n";
}

Также мы можем наследовать, добавляя новые функции:
class Person
  : public record<
        field<int, decltype("id"_s)>,
        field<std::string, decltype("first_name"_s)>,
        field<std::string, decltype("last_name"_s)>
    >
{
    public: void set_name(const std::string& f,const std::string& l) {
        (*this)["first_name"_s] = f;
        (*this)["last_name"_s] = l;
    };
};
int main(){
    Person p;
    p["id"_s] = 10;
    p.set_name("John","Smith");
    std::cout << p << "\n";
}

Итоговые объекты статически выводят тип поля по заданному ключу. А при использовании невалидного ключа не только генерируют ошибку компиляции, но и могут закрашать компилятор clang:



Ссылки


  1. User-defined literals (cppreference)
  2. N3599 Literal operator templates for strings (Richard Smith)
  3. [C++1y] Support n3599 — Literal operator templates for strings for C++1y (GCC Project)
Tags:
Hubs:
Total votes 20: ↑16 and ↓4+12
Comments11

Articles