[Из песочницы] Универсальный конструктор Auto

С приходом C++11 появилась возможность объявлять переменные с типом auto, а компилятор сам определил фактический тип переменной, на основе типа инициализируемого значения. Это удобно, когда мы хотим проинициализировать переменную тип которой слишком сложный, либо неизвестен, либо он нам не очень важен, либо просто для простоты.

Например:

auto f = [](){}; //указатель на функцию
auto r = foo(10); //тип возвращаемый функцией foo
for (auto i = 0; i < 10; i++){} 

… и т.д. То есть в левой части равенства у нас автоматический тип auto, а в правой части значение четко определенного типа. А теперь представим, что у нас все наоборот:
int a = auto(10);

Слева у нас четко описанный тип, а справа что-то неизвестное. Конечно в данном примере нет смысла вызывать универсальный конструктор, когда можно было просто присвоить к переменной a значение 10:
int a = 10;

Или в крайнем случае вызвать его конструктор:
int a(10);

А если это аргумент функции, например:
str::map myMap;
myMap.insert(pair('a', 10));

Метод insert шаблонного класса map ожидает четко указанный тип, но нам приходится писать «pair» снова и снова при каждом вызове. Хорошо если наш тип простой, а если там шаблон на шаблоне и шаблоном погоняет? Тут нам поможет автоматический конструктор:
myMap.insert(auto('a', 10));

Функция, Конструктор или Оператор auto, не важно что это, создаст нам какой-то объект, который подходит под описание входного параметра метода insert.

Но к сожалению в языке C++ пока нет такой методики создания объектов, но я надеюсь, что когда-нибудь она появится, а пока хочу представить свой вариант реализации такой задачи. Конечно же основная цель упростить написание кода, но не менее важная задача не навредить нашей программе: она не должна увеличится в объеме, замедлиться в выполнении и т.п. В идеале она должна быть идентична той, что если бы мы писали без конструктора auto.

И так. Нам нужно создать какой-то универсальный объект, который бы мог преобразоваться в запрашиваемый тип и сделать это на этапе компиляции. Конечно я не беру во внимание оптимизацию компиляции O0, Og и т.п. возьмем оптимизацию Os.

Наш объект (контейнер) должен принять все входные аргументы, сохранить их у себя, а при преобразовании к запрашиваемому типу попытаться вызвать его конструктор с передачей всего, что когда-то было передано ему.

Для начала нам понадобится универсальная переменная, способная хранить как копии объектов, так и указатели и ссылки. Хотя указатель и копия в данном случае одно и тоже, а вот со ссылкой сложнее:

template
struct Value {
	constexpr Value(T v): v(v) {}
	constexpr T get() {return v;}
	T v;
};
template
struct Value {
	constexpr Value(T& v): v(&v) {}
	constexpr T& get() {return *v;}
	T* v;
};

Здесь все относительно просто, принимаем любой аргумент и сохраняем его копию, а в случае со ссылкой преобразуем ее в указатель и обратно.

Сама специализация необходима, чтобы отсеять аргументы по ссылке, чтобы создать универсальность, а вот без преобразования в указатель компилятор отказывается выполнять на этапе компиляции, хотя казалось бы в чем разница, храним мы в указателе или в ссылке.

Теперь нам нужно создать универсальный контейнер с неограниченным числом аргументов произвольных типов:

template
struct Container {
	constexpr Container() {}
	template constexpr operator T() {return get();}
	template T constexpr get(Values&&... v) {return T((Values&&)v...);}
};
template
struct Container {
	constexpr Container(const Type&& arg, const Types&&... args): arg(arg), args((Types&&)args...) {}
	template constexpr operator T() {return get();}
	template T constexpr get(Values&&... v) {return args.get((Values&&)v..., arg.get());}
	Value arg;
	Container args;
};

Рекурсивный контейнер принимает неограниченное число аргументов различных типов и помещает первый аргумент к себе, а остальные во вложенный контейнер с оставшимися аргументами, пока не дойдем до последнего аргумента.

Также этот контейнер имеет оператор преобразования к любому требуемому типу вызывая рекурсивный метод get с вложением всех имеющихся аргументов.

Все аргументы передаются в качестве rvalue аргументов до самого конечного получателя Value, чтобы не потерять ссылки.

Ну и наконец сам универсальный конструктор Auto. Я его назвал с большой буквы, т.к. ключевое слово auto, сами понимаете, уже занято. А учитывая, что эта функция выполняет роль конструктора заглавная буква ей к лицу.

template
constexpr Container Auto(Types&&... args) {return Container((Types&&)args...);}

Напоследок переместим класс Value в private область класса Container и получится следующее:
template
struct Container {
	constexpr Container() {}
	template constexpr operator T() {return get();}
	template T constexpr get(Values&&... v) {return T((Values&&)v...);}
};

template
struct Container {
	constexpr Container(const Type&& arg, const Types&&... args): arg(arg), args((Types&&)args...) {}
	template constexpr operator T() {return get();}
	template T constexpr get(Values&&... v) {return args.get((Values&&)v..., arg.get());}
private:
	template
	struct Value {
		constexpr Value(T v): v(v) {}
		constexpr T get() {return v;}
		T v;
	};
	template
	struct Value {
		constexpr Value(T& v): v(&v) {}
		constexpr T& get() {return *v;}
		T* v;
	};
	Value arg;
	Container args;
};

template
constexpr Container Auto(Types&&... args) {return Container((Types&&)args...);}

Все преобразование выполняется на этапе компиляции и ни чем не отличается от прямого вызова конструкторов.

Правда есть небольшие неудобства — ни один суфлер не сможет вам предложить варианты входных аргументов.

Комментарии (11)

  • 24 ноября 2016 в 13:33

    0

    1. С парой все просто
    std::map myMap;
    myMap.insert(std::make_pair('a', 10));
    

    2. Почему ты решил, что этот auto сможет правильно разобрать, что нужно?

    class A
    {
       A(int, int);
    };
    
    class B
    {
       B(int, int);
    };
    
    class C
    {
        C(std::pair);
        C(A);
        C(B);
    };
    
    C obj = Auto(10, 10); // ???
    

    • 24 ноября 2016 в 13:41

      +1

      А в чем разница с этим:
      struct A {
      A (signed char v): v (v){}
      A (unsigned char v): v (v){}
      int v;
      };
      A a (1); //???

      если у вас несколько неявных кандидатов, то уж извините

      • 24 ноября 2016 в 14:04 (комментарий был изменён)

        0

        Дык, дело то в том, что в таком случае я напишу
        struct A {
           explicit A(signed char v): v(v){}
           explicit A(unsigned char v): v(v){}
           int v;
        };
        
        A a(1); // ошибка компиляции
        A a('a'); // ошибка компиляции
        A a((unsigned char)'a'); .// ok
        

        и проблема решена.

        А теперь посмотрим на случай с этим auto

        struct A
        {
           explicit A(int, int);
        };
        
        struct B
        {
           explicit B(int, int);
        };
        
        struct C
        {
            explicit C(const std::pair&);
            explicit C(const A&);
            explicit C(const B&);
        };
        
        std::vector vec;
        vec.push_back(A(10, 10));
        vec.push_back(B(10, 10));
        vec.push_back(std::make_pair(10, 10));
        vec.push_back(Auto(10, 10)); // ??? что здесь будет
        

  • 24 ноября 2016 в 13:37 (комментарий был изменён)

    +1

    В С++11 есть списки инициализации и можно писать так:


    std::map m{{'a',9}}; 
    m.insert({'b',10});

    • 24 ноября 2016 в 13:49

      0

      Вы опередили меня, только написал комментарий, как заметил ваш)
  • 24 ноября 2016 в 13:48

    +1

    Метод insert шаблонного класса map ожидает четко указанный тип, но нам приходится писать «pair» снова и снова при каждом вызове. Хорошо если наш тип простой, а если там шаблон на шаблоне и шаблоном погоняет?

    Для таких случаев есть синтаксис универсальной инициализации.
    Вместо вашего громоздкого
    myMap.insert(auto('a', 10));
    
    можно написать так
    myMap.insert({'a', 10});
    

  • 24 ноября 2016 в 13:51

    0

    Спасибо. видимо я это проморгал
  • 24 ноября 2016 в 14:54

    –1

    Еще немного, и C++ с PHP поменяются местами.
    Мне кажется, что auto лишь усложнит процесс отладки, ведь так?
    • 24 ноября 2016 в 15:03

      +1

      На самом деле нет.
      Если интересно, почему лучше использовать auto, чем не использовать — советую ознакомиться с 5 главой книги «Effective Modern C++» Скотта Майерса — она целиком посвящена этому ключевому слову и примерам его использования.
      • 24 ноября 2016 в 16:58

        –1

        На самом деле, процесс отладки auto только усложняет, по сути функция auto сводится к сокрытию ужасающего нагромождения шаблонных костылей. Если я использую какую-либо стороннюю библиотеку то хочу видеть что это за переменная, так я смогу понять какие операции с ней я смогу сделать. Нагромождение шаблонов и без того это скрывает, а auto вообще сводит на нет желание разбираться в чужом коде, и надо уповать только на хорошую документацию, что бывает далеко не всегда.
        • 24 ноября 2016 в 17:25

          +1

          А просто не нужно использовать auto так, когда оно приводит к усложнению читаемости кода. Тип переменной должен быть очевиден из одной строчки.

          К тому же, есть случаи, когда auto используется для неявного вывода типа, например:
          auto z = x + y, где типы переменных x и y являются шаблонными.

© Habrahabr.ru