[Из песочницы] Универсальный конструктор 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 являются шаблонными.