С приходом C++11 появилась возможность объявлять переменные с типом auto, а компилятор сам определил фактический тип переменной, на основе типа инициализируемого значения. Это удобно, когда мы хотим проинициализировать переменную тип которой слишком сложный, либо неизвестен, либо он нам не очень важен, либо просто для простоты.
Например:
… и т.д. То есть в левой части равенства у нас автоматический тип auto, а в правой части значение четко определенного типа. А теперь представим, что у нас все наоборот:
Слева у нас четко описанный тип, а справа что-то неизвестное. Конечно в данном примере нет смысла вызывать универсальный конструктор, когда можно было просто присвоить к переменной a значение 10:
Или в крайнем случае вызвать его конструктор:
А если это аргумент функции, например:
Метод insert шаблонного класса map ожидает четко указанный тип, но нам приходится писать «pair<char, int>» снова и снова при каждом вызове. Хорошо если наш тип простой, а если там шаблон на шаблоне и шаблоном погоняет? Тут нам поможет автоматический конструктор:
Функция, Конструктор или Оператор auto, не важно что это, создаст нам какой-то объект, который подходит под описание входного параметра метода insert.
Но к сожалению в языке C++ пока нет такой методики создания объектов, но я надеюсь, что когда-нибудь она появится, а пока хочу представить свой вариант реализации такой задачи. Конечно же основная цель упростить написание кода, но не менее важная задача не навредить нашей программе: она не должна увеличится в объеме, замедлиться в выполнении и т.п. В идеале она должна быть идентична той, что если бы мы писали без конструктора auto.
И так. Нам нужно создать какой-то универсальный объект, который бы мог преобразоваться в запрашиваемый тип и сделать это на этапе компиляции. Конечно я не беру во внимание оптимизацию компиляции O0, Og и т.п. возьмем оптимизацию Os.
Наш объект (контейнер) должен принять все входные аргументы, сохранить их у себя, а при преобразовании к запрашиваемому типу попытаться вызвать его конструктор с передачей всего, что когда-то было передано ему.
Для начала нам понадобится универсальная переменная, способная хранить как копии объектов, так и указатели и ссылки. Хотя указатель и копия в данном случае одно и тоже, а вот со ссылкой сложнее:
Здесь все относительно просто, принимаем любой аргумент и сохраняем его копию, а в случае со ссылкой преобразуем ее в указатель и обратно.
Сама специализация необходима, чтобы отсеять аргументы по ссылке, чтобы создать универсальность, а вот без преобразования в указатель компилятор отказывается выполнять на этапе компиляции, хотя казалось бы в чем разница, храним мы в указателе или в ссылке.
Теперь нам нужно создать универсальный контейнер с неограниченным числом аргументов произвольных типов:
Рекурсивный контейнер принимает неограниченное число аргументов различных типов и помещает первый аргумент к себе, а остальные во вложенный контейнер с оставшимися аргументами, пока не дойдем до последнего аргумента.
Также этот контейнер имеет оператор преобразования к любому требуемому типу вызывая рекурсивный метод get с вложением всех имеющихся аргументов.
Все аргументы передаются в качестве rvalue аргументов до самого конечного получателя Value, чтобы не потерять ссылки.
Ну и наконец сам универсальный конструктор Auto. Я его назвал с большой буквы, т.к. ключевое слово auto, сами понимаете, уже занято. А учитывая, что эта функция выполняет роль конструктора заглавная буква ей к лицу.
Напоследок переместим класс Value в private область класса Container и получится следующее:
Все преобразование выполняется на этапе компиляции и ни чем не отличается от прямого вызова конструкторов.
Правда есть небольшие неудобства — ни один суфлер не сможет вам предложить варианты входных аргументов.
Например:
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<char, int> myMap;
myMap.insert(pair<char, int>('a', 10));
Метод insert шаблонного класса map ожидает четко указанный тип, но нам приходится писать «pair<char, int>» снова и снова при каждом вызове. Хорошо если наш тип простой, а если там шаблон на шаблоне и шаблоном погоняет? Тут нам поможет автоматический конструктор:
myMap.insert(auto('a', 10));
Функция, Конструктор или Оператор auto, не важно что это, создаст нам какой-то объект, который подходит под описание входного параметра метода insert.
Но к сожалению в языке C++ пока нет такой методики создания объектов, но я надеюсь, что когда-нибудь она появится, а пока хочу представить свой вариант реализации такой задачи. Конечно же основная цель упростить написание кода, но не менее важная задача не навредить нашей программе: она не должна увеличится в объеме, замедлиться в выполнении и т.п. В идеале она должна быть идентична той, что если бы мы писали без конструктора auto.
И так. Нам нужно создать какой-то универсальный объект, который бы мог преобразоваться в запрашиваемый тип и сделать это на этапе компиляции. Конечно я не беру во внимание оптимизацию компиляции O0, Og и т.п. возьмем оптимизацию Os.
Наш объект (контейнер) должен принять все входные аргументы, сохранить их у себя, а при преобразовании к запрашиваемому типу попытаться вызвать его конструктор с передачей всего, что когда-то было передано ему.
Для начала нам понадобится универсальная переменная, способная хранить как копии объектов, так и указатели и ссылки. Хотя указатель и копия в данном случае одно и тоже, а вот со ссылкой сложнее:
template<typename T>
struct Value {
constexpr Value(T v): v(v) {}
constexpr T get() {return v;}
T v;
};
template<typename T>
struct Value<T&> {
constexpr Value(T& v): v(&v) {}
constexpr T& get() {return *v;}
T* v;
};
Здесь все относительно просто, принимаем любой аргумент и сохраняем его копию, а в случае со ссылкой преобразуем ее в указатель и обратно.
Сама специализация необходима, чтобы отсеять аргументы по ссылке, чтобы создать универсальность, а вот без преобразования в указатель компилятор отказывается выполнять на этапе компиляции, хотя казалось бы в чем разница, храним мы в указателе или в ссылке.
Теперь нам нужно создать универсальный контейнер с неограниченным числом аргументов произвольных типов:
template<typename... Types>
struct Container {
constexpr Container() {}
template<typename T> constexpr operator T() {return get<T>();}
template<typename T, typename... Values> T constexpr get(Values&&... v) {return T((Values&&)v...);}
};
template<typename Type, typename... Types>
struct Container<Type, Types...> {
constexpr Container(const Type&& arg, const Types&&... args): arg(arg), args((Types&&)args...) {}
template<typename T> constexpr operator T() {return get<T>();}
template<typename T, typename... Values> T constexpr get(Values&&... v) {return args.get<T>((Values&&)v..., arg.get());}
Value<Type> arg;
Container<Types...> args;
};
Рекурсивный контейнер принимает неограниченное число аргументов различных типов и помещает первый аргумент к себе, а остальные во вложенный контейнер с оставшимися аргументами, пока не дойдем до последнего аргумента.
Также этот контейнер имеет оператор преобразования к любому требуемому типу вызывая рекурсивный метод get с вложением всех имеющихся аргументов.
Все аргументы передаются в качестве rvalue аргументов до самого конечного получателя Value, чтобы не потерять ссылки.
Ну и наконец сам универсальный конструктор Auto. Я его назвал с большой буквы, т.к. ключевое слово auto, сами понимаете, уже занято. А учитывая, что эта функция выполняет роль конструктора заглавная буква ей к лицу.
template<typename... Types>
constexpr Container<Types...> Auto(Types&&... args) {return Container<Types...>((Types&&)args...);}
Напоследок переместим класс Value в private область класса Container и получится следующее:
template<typename... Types>
struct Container {
constexpr Container() {}
template<typename T> constexpr operator T() {return get<T>();}
template<typename T, typename... Values> T constexpr get(Values&&... v) {return T((Values&&)v...);}
};
template<typename Type, typename... Types>
struct Container<Type, Types...> {
constexpr Container(const Type&& arg, const Types&&... args): arg(arg), args((Types&&)args...) {}
template<typename T> constexpr operator T() {return get<T>();}
template<typename T, typename... Values> T constexpr get(Values&&... v) {return args.get<T>((Values&&)v..., arg.get());}
private:
template<typename T>
struct Value {
constexpr Value(T v): v(v) {}
constexpr T get() {return v;}
T v;
};
template<typename T>
struct Value<T&> {
constexpr Value(T& v): v(&v) {}
constexpr T& get() {return *v;}
T* v;
};
Value<Type> arg;
Container<Types...> args;
};
template<typename... Types>
constexpr Container<Types...> Auto(Types&&... args) {return Container<Types...>((Types&&)args...);}
Все преобразование выполняется на этапе компиляции и ни чем не отличается от прямого вызова конструкторов.
Правда есть небольшие неудобства — ни один суфлер не сможет вам предложить варианты входных аргументов.