С приходом 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<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...);}

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

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

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


  1. DeadKnight
    24.11.2016 13:33
    +2

    1. С парой все просто

    std::map<char, int> 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<int, int>);
        C(A);
        C(B);
    };
    
    C obj = Auto(10, 10); // ???
    


    1. Ivanbk
      24.11.2016 13:41
      +1

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

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


      1. DeadKnight
        24.11.2016 14:04

        Дык, дело то в том, что в таком случае я напишу

        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<int, int>&);
            explicit C(const A&);
            explicit C(const B&);
        };
        
        std::vector<C> 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)); // ??? что здесь будет
        


  1. Akon32
    24.11.2016 13:37
    +6

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


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


    1. Daniro_San
      24.11.2016 13:49

      Вы опередили меня, только написал комментарий, как заметил ваш)


    1. bataloviluxa
      24.11.2016 21:59

      Не сработает в С++11. Тип std::initializer_list может хранить только один тип данных.
      auto t = {1.0, 2.0, 3.0};//t — std::initializer_list.

      В стандарт хотят(или уже) включить автоматическое создание пары и кортежа
      auto pair = {'a',9};//std::pair<char,in>
      auto cort = {'a',9,10.0};//std::tupple<char,in,double>


      1. Antervis
        25.11.2016 06:09
        +2

        но синтаксис через {} относится не только к спискам инициализации. В некоторых контекстах это как раз-таки создание объекта через конструктор с подходящей сигнатурой.


      1. TheCalligrapher
        25.11.2016 09:12
        +2

        Все прекрасно сработает именно в С++11. Синтаксис {} в данном случае не имеет никакого отношения к std::initializer_list.


      1. HolyBlackCat
        25.11.2016 11:07

        Сработает, потому что это не std::initializer_list.


        В обычной инициализации структур используется тот же синтаксис и никаких проблем не возникает, хотя типы разные. (Это было еще в С.)


        struct S {int a; const char *b;};
        S s = {1, "2"};


    1. diggaz
      25.11.2016 11:07
      +1

      можно использовать метод emplace, который сразу вызовет конструктор:

      std::map<int, std::string> map;
      map.emplace(1, "text1");
      

      http://cpp.sh/6rkh


  1. Daniro_San
    24.11.2016 13:48
    +2

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

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


  1. Ivanbk
    24.11.2016 13:51
    +1

    Спасибо. видимо я это проморгал


  1. ilyaplot
    24.11.2016 14:54
    +1

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


    1. Jigglypuff
      24.11.2016 15:03
      +3

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


      1. rikert
        24.11.2016 16:58
        -2

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


        1. DistortNeo
          24.11.2016 17:25
          +6

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

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


          1. rikert
            24.11.2016 19:54
            +1

            К сожалению нужно-не нужно, это не более чем демагогия об идеальном программном коде. Сколько бы минусов не поставили мне и плюсов вам, код не станет лучше, а auto только будет сбивать с толку и усложнять код. Длинный и запутанный тип нельзя простым укорачиванием до слова auto упростить и сделать ясным и понятным.


            1. DistortNeo
              24.11.2016 21:54
              +2

              Ну почему же? Упростить decltype(std::declval() + std::declval()) до auto вполне можно.
              Или std::map<std::string, std::tuple<int, float>>::const_iterator до auto в цикле for.
              А вот писать auto x = 1 нельзя — за такое надо бить по рукам.


              1. rikert
                24.11.2016 22:11
                -1

                На правах своего мнения скажу, что C++ он вообще такой, он именно и состоит из пересекающихся шаблонов, и если программист не умеет их читать какими бы большими они не были то ему надо научиться. Мне проще читать std::map<std::string, std::tuple<int, float>>::const_iterator, я понимаю что это, для чего это и что с этим я могу сделать, я сразу вижу свойства, а увидев auto мне надо рыть, что там вообще такое в принципе или анализировать нижеследующий код и пытаться по операциям с этой auto предсказать что это за тип. Auto может быть полезен для новичков, но они впоследствии столкнутся с этой же проблемой. В общем на мой взгляд auto не сочетается с природой C++. А так да, вроде бы удобно, но только на бумаге.


                1. Antervis
                  25.11.2016 06:11
                  +1

                  а вы увидев begin/end над контейнером не понимаете, что он возвращает итератор?


                1. Deosis
                  25.11.2016 07:21
                  +1

                  void f(std::map<std::string, std::tuple<int, float>>& map)
                  {
                  auto it = map.Begin();
                  std::map<std::string, std::tuple<int, float>>::const_iterator end = map.End();
                  ...
                  }
                  

                  По вашему второй вариант лучше? Повторять длинное описание типа каждый раз, когда потребуется объявить переменную производного типа?
                  Тем более, что тип переменной очевиден.


                  1. iCpu
                    25.11.2016 08:03

                    Я не противник auto, мне это ключевое слово нравится, но, справедливости ради, в данном случае я бы переписал код так:

                    using TextValueMap = std::map<std::string, std::tuple<int, float>>;
                    void f(TextValueMap& tvmap)
                    {
                    TextValueMap::iterator Uter = tvmap.begin();
                    /*...*/
                    }
                    


                    1. rotor
                      25.11.2016 11:58
                      +3

                      Зачем? std::map::begin() всегда возвращает iterator или const_iterator.
                      Если через какое то время вы поменяете сигнатуру функции f на


                      void f(const TextValueMap& tvmap)

                      то вам придется изменять также строку


                      TextValueMap::iterator Uter = tvmap.begin();

                      на


                      TextValueMap::const_iterator Uter = tvmap.begin();

                      Компилятор конечно подскажет, но зачем отвлекаться?
                      Использование auto в данном случае не вносит никаких неоднозначностей, упрощает чтение кода и облегчает его поддержку. Так зачем усложнять?


                      1. iCpu
                        25.11.2016 12:44
                        -1

                        Мой ответ относился не к использованию слова auto в коде, а к сокращению размеров записи с помощью выражений using, так как предыдущий комментатор высказался именно на тему объёмов писанины. Ключевое слово auto, всё-таки, создано не только и не столько для этого.

                        И оффтопом
                        В Qt5 проход по контейнерам с ключами в новом стиле for даст весьма забавный эффект. В следующем коде i будет типа short, а не QPair <int,short>, как было бы с std::map и std::pair<int,short>, соответственно.
                        	QMap<int,short> m; m[0]=1; m[1]=2;
                        	for (auto & i: m) {
                        		qDebug() << i; // 1 2
                        	}
                        

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


                        1. Antervis
                          25.11.2016 13:18
                          +1

                          И оффтопом

                          там итератор странно реализован: operator * вместо пары ключ/значение возвращает значение, но имеет методы key/value


                  1. semenyakinVS
                    25.11.2016 13:46

                    В данном случае auto действительно удобно. iCpu, как я понял, имеет в виду, что есть достаточное количество ситуаций, в которых auto делает код менее очевидным. Например, если бы map в вашем примере был не аргументом функции прямо на строчку выше, а, например, полем класса, то было бы уже не так неудобно:

                    auto it = _map.begin();
                    


                    Как тут тип понять какой тип у мапы? Что если _map — константная и const_iterator вернёт? И это очень простой пример.

                    Я стараюсь применять auto как можно реже и только там, где тип очевиден из контекста максимум несколькими строками выше. Например, вот в таких конструкциях:

                    auto thePointer = dynamic_cast<TYPE *>(theBasePointer);
                    


                    Что касается циклов с итераторам — на мой взгляд, не так часто в прикладном коде возникает необходимость использования итераторов напрямую. Чаще range-based, где не нужно использовать итераторы. Логику, где нужно оперировать итераторами напрямую, можно спрятать в функции (и, как правило, это обосновано дизайном).


                    1. DistortNeo
                      25.11.2016 14:03
                      +1

                      В данном случае auto действительно удобно. iCpu, как я понял, имеет в виду, что есть достаточное количество ситуаций, в которых auto делает код менее очевидным. Например, если бы map в вашем примере был не аргументом функции прямо на строчку выше, а, например, полем класса, то было бы уже не так неудобно:

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

                      Как тут тип понять какой тип у мапы? Что если _map — константная и const_iterator вернёт? И это очень простой пример.

                      А вы тогда сразу cbegin вызывайте — проблем не будет.


                    1. Antervis
                      25.11.2016 14:06
                      +1

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

                      auto itMax = std::max_element(begin(_records),end(_records));
                      


              1. TheShock
                25.11.2016 04:30

                А вот писать [...] нельзя — за такое надо бить по рукам.

                Меня всегда интересовало — так где грань? Пока я для себя не нашел однозначного ответа.


      1. Antervis
        25.11.2016 06:13

        надо лишь добавить, что всё-таки лучше воздержаться от некоторых советов Майерса:

        auto highPriority = static_cast<bool>(features(w)[5]);
        


        1. ZekaM
          25.11.2016 11:08

          А здесь разве не всегда bool тип будет? Вроде тип описан справа, поэтому слева ставим auto.


          1. Antervis
            25.11.2016 11:33
            +1

            там в примере features типа std::vector, operator[] которого возвращает не bool, а прокси класс, эмулирующий ссылку на бит.


    1. 4e1
      24.11.2016 21:59

      Не особо, а, например, в случае for (auto item: someContainer) — auto вообще в самый раз — какого типа someContainer — неважно, более того его можно спокойно менять vector/list/set… не переписывая при этом весь код, где по нему итерируются.

      Единственное, на что следует обращать внимание — чтобы область видимости «auto» не выходила за пределы функции или блока. Это правило очень хорошо ограничивают ООП и обязательное разделение класса на .h и .cpp файлы — нельзя объявить метод класса, возвращающий auto, а реализацию написать в .cpp


      1. 4e1
        24.11.2016 22:12

        сори, тип контейнера ни при чем, но тип элемента контейнера тоже менять можно, например всякие int, int8_t qint8 и их 16, 32, 64 сородичи и т. д.


      1. ZekaM
        25.11.2016 11:10

        А так не лучше будет?
        for (const auto& item: someContainer)


        1. 4e1
          25.11.2016 12:03

          это уже зависит от ситуации


        1. rotor
          25.11.2016 12:07

          На самом деле, почти всегда лучше писать


          for (auto&& item: someContainer)

          Если someContainer::iterator::operator*() возвращает ссылку или константную ссылку, то item таковым и будет, если rlalue ссылку то возвращаемое значение будет перемещено в item без создания временного объекта.


          1. Antervis
            25.11.2016 13:19

            в некоторых случаях может резолвнуться в неконстантную ссылку, а мутабельный доступ к контейнеру с implicit shared memory (например, как в контейнерах Qt) приведет к лишнему копированию данных


          1. TheCalligrapher
            25.11.2016 19:34

            возвращаемое значение будет перемещено в item без создания временного объекта

            Чего??? item в данном случае является ссылкой (вы пытастесь воспользоваться мейерсовской концепцией "универсальной сслыки"). Ничего в item перемещаться, разумеется, не будет.


            Никаких временных объектов не создается и в огигинальном варианте с lvalue-ссылкой.


      1. semenyakinVS
        25.11.2016 14:05
        -1

        Хм. Если имеется в виду range-based цикл, я совсем не понимаю почему тут-то нельзя указать тип содержимого контейнера. Места займёт не так много — не нужно городить шаблонные конструкции с итераторами. А код понятнее станет. Сразу ясно, что смысл: мы перебираем элементы некой коллекции. Не важно какой, важно элементы какого типа в ней лежат (и это действительно важно — ведь мы в теле цикла будем оперировать объектами этого типа).


        1. semenyakinVS
          27.11.2016 04:45

          Если не секрет — объясните, что я не так написал? Я серьёзно. Не хотел никого троллить или флудить. Действительно хочу понять — какой смысл прятать тип элементов коллекции в range-based циклах (если речь действительно о них шла)?


          1. 0xd34df00d
            27.11.2016 04:51
            +1

            А смысл в этих типах? В конце концов, вам важно лишь то, что вы с ними делаете, а это уже видно из тела цикла. Какая разница, перебираете ли вы std::string, QString или ещё что, если у вас в теле цикла out << trim(str);?


            Если так уж хочется посмотреть на статический тип элементов — наведите курсор на соответствующий идентификатор, хорошая IDE вам покажет, а плохой не надо пользоваться.


          1. Antervis
            28.11.2016 06:16
            +1

            Чтобы, если потом окажется, что в целях оптимизации надо заменить std::list на std::vector, достаточно было поменять тип в одном месте. Меня самого совсем недавно auto спас от сотен строк кода на шаблонах или макросах (нужна была таблица, корректно отображающая одномерные/двумерные вектора любых числовых типов)


  1. mihaild
    24.11.2016 18:44
    +2

    auto f = [](){}; //указатель на функцию
    

    #include <type_traits>
    
    int main() {
        auto f1 = [](){};
        static_assert(std::is_pointer<decltype(f1)>::value, "f1 is not pointer");
    }

    fp.cpp:5:5: error: static_assert failed "f1 is not pointer"
        static_assert(std::is_pointer<decltype(f1)>::value, "f1 is not pointer");
        ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1 error generated.


    Это всё-таки лямбда, а не указатель (в частности, штука с потенциально дорогим копированием).


    1. abikineev
      25.11.2016 03:13

      Вам дорого копировать 1 байт?


      1. mihaild
        25.11.2016 03:23

        Где вы тут 1 байт увидели?


    1. Antervis
      25.11.2016 06:18
      +2

      А вот тут указатель на функцию:

      auto f1 = +[](){};
      


    1. Satus
      25.11.2016 22:43

      Это всё-таки лямбда, а не указатель (в частности, штука с потенциально дорогим копированием).
      В данном случае будет просто структура с определенным оператором `()`.


      1. mihaild
        26.11.2016 01:19

        В данном случае — да. Поэтому написано «потенциально» (на практике — в случаях, когда тяжелые объекты захватываются по значению).


  1. Dark_Daiver
    24.11.2016 19:26
    +1

    >Но к сожалению в языке C++ пока нет такой методики создания объектов
    И я очень надеюсь что она и не появится. Ибо чревато всякими неожиданными эффектами


    1. Deosis
      25.11.2016 07:28

      К счастью

      в языке C++ пока нет такой методики создания объектов
      Так как она никак не сочетается с шаблонами и перегрузками функций.
      template<int length, typename Type>
      class SomeClass {
      public:
      SomeClass(Complex c);
      private:
      ...
      }
      template<int length, typename Type>
      void f(SomeClass<length, Type> some);
      
      f(SomeClass<10, int>(1.0));
      f(Auto(1.0));//???
      

      В первом варианте компилятор найдет неявное преобразование из double в Complex.
      Во втором придется указывать шаблонные параметры


  1. crea7or
    24.11.2016 20:17
    -1

    Дико бесит когда встречаю в чужих c# исходниках var. Приходится переключаться с понимания задачи на поиск того, что в этом var хранят сейчас и auto такой же трэш.


    1. Dark_Daiver
      24.11.2016 20:21
      +2

      Зато с auto итерирование по коллекциям перестало быть болью


      1. crea7or
        24.11.2016 22:41
        -1

        Кажется что да, наверно правильно, а потом понимаешь, что чтобы узнать про тип объектов в коллекции придётся IDE напрягать.


        1. Dark_Daiver
          24.11.2016 22:49
          +1

          Обычно, где-то неподалеку есть объявление самой коллекции, а так да. Но это цена которую лично я готов заплатить за удобный for


        1. TheCalligrapher
          25.11.2016 05:01
          +3

          Еще со времен С твердили: учитесь и писать и читать type-agnostic код. В не-декларативных statements языков С и С++ не должно быть никаких упоминаний конкретных типов. Именам типов место в декларациях и только в декларациях. Type-agnostic код прекрасно читаем, надо только набраться смелости и отбросить в сторону костыли, которыми являлись постоянные упоминания имен типов.


          Но дурные привычки-костыли продолжают жить: молодежь тупо настаивает на явном приведении типа результата malloc в С или использовании имен типов в sizeof, ибо так якобы "надо, чтобы знать с каким типом мы работаем". И несмотря на все усилия более продвинутой мировой C/C++ community, манера тащить за собой имена типов куда надо и куда не надо умирать никак не хочет, ни в С, ни в С++.


          Надеюсь, что привлекательность новых возможностей современного С++ победит дурные привычки в С++. А вот как навести порядок в рядах С-шников — не ясно.


          1. semenyakinVS
            25.11.2016 14:07
            -1

            Еще со времен С твердили: учитесь и писать и читать type-agnostic код


            Кто твердил и чем именно это обосновывалось?


            1. 0xd34df00d
              25.11.2016 23:14
              +1

              Это трудно обосновывать, потому что это двоякое утверждение.


              С моей точки зрения, во-первых, сигнатуры функций — это важно, сигнатура выражает некоторое намерение, что функция будет делать. Если у вас boost::optional<Foo> FindFoo(), значит, она может его не найти, и это сразу видно.


              С другой стороны, после того, как я написал auto widget = CreateAWidget();, мне не столь важно, какой у него тип.


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


              1. semenyakinVS
                26.11.2016 04:23

                Вообще, я вот подумал — у меня пока не было опыта работы с крупным С++ проектом, в котором активно использовалось бы auto. Негативно отношусь к нему в большой степени из-за воспоминаний, оставленных нестрого типизированными скриптовыми языками (python и action script), большой код в которых часто воспринимался очень трудно.

                Может, вы знаете какой-нибудь open source проект на С++, который активно использовал бы на type-agnostic код? Я бы глянул. На примере анализа кода подобного проекта можно было бы понять насколько хорош (ну, или плох) auto.


                1. 0xd34df00d
                  26.11.2016 06:36
                  +2

                  Я сходу вряд ли готов дать релевантный пример.


                  С другой стороны, у меня есть опыт работы над достаточно крупными проектами на хаскеле, который статически и строго типизирован, но вместе с тем указания типов в подавляющем большинстве случаев компилятору вообще не нужны. Правило «явно писать сигнатуры только у top-level-функций, и то скорее в качестве документации» — оно хорошее и вполне рабочее.


                  1. semenyakinVS
                    27.11.2016 04:41

                    и то скорее в качестве документации


                    На мой взгляд, одной из главных задач языков высокого уровня является как раз-таки умение быть документацией. И чем более подробной — тем лучше. Я не говорю, что код при этом не должен быть лаконичным — слишком много текста делается код менее читабельным. Однако, как по мне, краткость не должна быть самоцелью.

                    На мой взгляд, несколько секунд, сэкономленные на указание полного типа, могут обернуться минутами затрат на ознакомление с кодом нового человека… Увы, как я уже писал выше, более или менее объективно рассудить нас сможет только проект, активно использующий auto. Если как-нибудь столкнусь с таким и не забуду о нашем разговоре — можно будет рассмотреть его в качестве примере.

                    P.S.: Кстати, с учётом частых дискуссий по теме auto (не только на хабре — среди моих знакомых программистов эта фича С++11 была воспринята наиболее противоречиво и часто вызывает жаркие споры), на мой взгляд, была бы полезной целая статья с разбором плюсов и минусов auto на примере анализа кода какого-нибудь реального open-source проекта. Возможно, подобная статья вывела бы тему использования auto из категории «религиозных» тем. Если когда-нибудь столкнусь с подобным проектом и на тот момент не будет статьи — могу написать. Как думаете, имеете смысл? Или, возможно, вы знаете какую-нибудь существующую статью аналогичного содержания?


                    1. 0xd34df00d
                      27.11.2016 04:55
                      +3

                      Не сможет проект нас рассудить. И разборы плюсов-минусов на примере проекта тоже не смогут нас рассудить.


                      По крайней мере, до тех пор, пока мы с вами не придумаем объективные критерии. Свяжем количество багов на тысячу строк кода с использованием auto, например. Или среднее время от знакомства человека с проектом до первого pull request'а. Или, не знаю, время компиляции, в конце концов. Тоже объективная метрика, пусть и не столь полезная.


                      А читабельность для всех разная. Вам удобно знать тип элемента в range-based for loop, мне не столь обязательно. Моему коллеге удобно написать прототип на лиспе и нафигачить потом аннотаций типов, когда прототип будет готов, мне удобнее сначала продумать иерархию типов и написать сигнатуры нужных функций, а функции определить как undefined, а когда код начнёт тайпчекаться, выключить мозг и пойти писать тела функций.


                      Субъективно оно всё, короче. Можно только обсуждать, какой субъективности в среднем больше. Факт наличия этого треда показывает, что type-agnostic-субъективщины, возможно, больше.


                    1. DarkEld3r
                      28.11.2016 14:09
                      +1

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

                      Не знаю у кого как, но у меня ознакомление с новой кодовой базой всегда происходит непросто. Вот только (опять же, лично мне) при этом всё равно важнее логика происходящего, а не типы. Ну серьёзно, чем поможет какой-нибудь Entity<Run<Some>>? Да, можно скрупулёзно изучить все составляющие части, а потом ещё иерархии классов, специализации шаблонов и т.д. Но появляется риск забыть, что собственно искали. (:


                      Опять же, в моём опыте даже в весьма качественных проектах бывали "странные" именования у типов (и модулей). То есть, всё равно надо вникать в предметную область и код. Снова сошлюсь на личный опыт — мне проще постепенно расширять область понимания. Если закапываться в типы и иерархии, то это больше сбивает.


                      Справедливости ради, скажу, что IDE (в моём случае, QtCreator) не всегда может показать тип, если там "несколько уровней" auto и это действительно бывает неудобно.


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


    1. Alesh
      24.11.2016 21:05
      +1

      Все лучшее хорошо в меру, иначе оно станет врагом хорошего)


    1. DistortNeo
      24.11.2016 22:05
      +1

      Ох как вы будете тогда злиться от новых возможностей C# 7 (анонимные классы).
      А ещё существуют языки с динамической типизацией типа Python: там каждая переменная — это var.


      1. crea7or
        24.11.2016 22:39

        Когда пишу на языках с var'ами — я к этому готов, они изначально таковы, а в с++ мне это кажется лишним. Если это используют там, где предполагается — то ничего особо страшного(да и не особо нужно-то). Но ведь будут куда ни попадя auto писать.


        1. Sirikid
          25.11.2016 01:18

          (Оффтопик) А как вы относитесь к языкам с глобальным выводом типов?


      1. 0xd34df00d
        25.11.2016 23:09

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


        Лучше бы хаскель вспомнили.


  1. rotor
    24.11.2016 21:07
    +1

    У вас по всему тексту встречаются такие вот конструкции:


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

    Что вы хотели сказать вот этим: ((Types&&)args...)?
    Мне кажется, в данном случае будет уместнее использовать std::forward:


    template<typename... Types>
    constexpr Container<Types...> Auto(Types&&... args) {return Container<Types...>(std::forward(args)...);}

    Выглядит так, как будто вы замышляли идеальную передачу, а выполняете странное приведение типов в стиле C.
    кроме того, вот эта фраза


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

    Явно свидетельствует, что вы путаете rvalue и универсальные ссылки.


    1. HolyBlackCat
      25.11.2016 17:54

      Все-таки ((Types&&)args...) — это тоже идеальная передача, и работает это так же, как std::forward().


      1. rotor
        25.11.2016 19:40

        Совсем нет. Сравните: С-style-cast и std:forward


        1) When the C-style cast expression is encountered, the compiler attempts to interpret it as the following cast expressions, in this order:
        a) const_cast<new_type>(expression);
        b) static_cast<new_type>(expression), with extensions: pointer or reference to a derived class is additionally allowed to be cast to pointer or reference to unambiguous base class (and vice versa) even if the base class is inaccessible (that is, this cast ignores the private inheritance specifier). Same applies to casting pointer to member to pointer to member of unambigous non-virtual base;
        c) static_cast (with extensions) followed by const_cast;
        d) reinterpret_cast<new_type>(expression);
        e) reinterpret_cast followed by const_cast.

        Возможная реализация для std:forward (g++ bits/move.h):


        template<typename T>
        constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept
        {
            return static_cast<T&&>(t);
        }
        template<typename T>
        constexpr T&& forward(typename std::remove_reference<T>::type&& t) noexcept
        {
            static_assert(!std::is_lvalue_reference<T>::value, "template argument substituting T is an lvalue reference type");
            return static_cast<T&&>(t);
        }

        Видите разницу?


        Дополнително: 1 2 3 4


        1. DistortNeo
          25.11.2016 21:17

          Мне всегда было интересно, в каких случаях (реальных) возможно срабатывание assert во втором forward и почему нельзя просто писать static_cast<T&&> вместо std::forward?


          1. rotor
            25.11.2016 23:04

            Это такая защита от не правильного использования. std::forward предполагает использование именно с шаблонным параметром, но если кто-то вдруг захочет странного и напишет что то вроде этого:


            std::forward<int&>(5)

            то сработает защита. В этом случае аргумент является rvalue, поэтому компилятор выбирает вторую перегрузку, но тип ссылка на lvalue. пример


          1. TheCalligrapher
            25.11.2016 23:11

            Функциональной разницы нет.


            Вторая версия функции предназначена только для того, чтобы предотвратить ошибки/хаки типа std::forward<int &>(42), т.е. ситуации, когда пользователь случайно или намеренно указывает "нелегальную" комбинацию типа и значения. То есть вторая версия существует только ради этого static_assert внутри. Если бы мы не задавались целью следить за корректностью использования std::forward, то достатчоно было бы первой перегрузки.


        1. HolyBlackCat
          29.11.2016 22:42

          Я вижу только дополнительный static_assert() и защиту от неправильного использования.


          Но я не могу придумать пример, когда (T &&)value не сработало бы как идеальная передача, если T — тип универсальной ссылки, выбранный компилятором автоматически. Можете привести такой пример?


  1. Ivanbk
    24.11.2016 21:52

    Но, самое главное, совершенно не ясно, почему вы проигнорировали вариант
    myMap.insert({ 'a', 10 });
    К сожалению, как я уже сказал, я не знал про списки инициализации. я думал, что это применимо только к инициализации переменных
    MyObj obj = {1, 2, 3};
    и по этому мне пришлось изобретать велосипед.
    У типа int нет и не может быть конструктора
    понятно, что у int нет конструктора, я просто привел простой пример.
    Что вы хотели сказать вот этим: ((Types&&)args...)?
    если конечный конструктор принимает ссылку на объект, то без этого преобразования конструктор будет пытаться получить ссылку на промежуточную переменную, а не на первоначальный объект.

    Спасибо всем за обсуждение, вы повысили мои познания языка. Где как не в споре рождается истина.


  1. TheCalligrapher
    24.11.2016 21:59

    когда можно было просто присвоить к переменной a значение 10

    int a = 10 — это не присваивание, а инициализауция.


    Или в крайнем случае вызвать его конструктор:

    У типа int нет и не может быть конструктора. Конструкторы в языке С++ бывают только у класс-типов. int — это не класс-тип.


    Более того, статься совершенно не объясняет, зачем все это нужно. Пример с map::insert — мимо кассы, обо эта задача в С++ давно решается униформной инициализацией


    myMap.insert({ 'a', 10 });

    Причем этот вариант превосходит все предложенные варианты тем, что сразу создает пару правильного типа — std::pair<const char, int> — обратите внимание на наличие const перед char. Забывать указывать этот const — популярный огрех среди начинающих программистов. Это приводит к лишней промежуточной конверсии. К сожалению, от аналогичной проблемы же страдает и популярный вариант с make_pair. И от такой же проблемы страдает и ваш вариант.


    Но, самое главное, совершенно не ясно, почему вы проигнорировали вариант


    myMap.insert({ 'a', 10 });

    который в данном как раз и будет выбирать способ инициализации автоматически именно по типу параметра.


    1. TheCalligrapher
      24.11.2016 22:40
      +1


      Также вызывает недоумение ваше


      auto f = [](){}; //указатель на функцию

      Хоть "captureless" лямбды и приводимы к указателю на функцию, в данном случае f — отнюдь не указатель на функцию, а все таки специальный функциональный объект (closure object).


  1. elephanten
    24.11.2016 21:59

    В С++ есть масса замечательных вещей, которые умиляют. На мой взгляд, первым идет оператор «сиськи» operator()(), а вторым — ваши три пары разных скобок подряд [](){}.


    1. Satus
      25.11.2016 22:48

      Ну можете писать []{}, если не нужно передавать параметры.


      1. 0xd34df00d
        25.11.2016 23:16

        А можно написать [](){}(), чтобы сразу вызвать.


  1. AxisPod
    25.11.2016 06:03
    -3

    Что за бред я только что прочитал? Вы предлагаете функциональность, которая позволит сломать какой-либо код написав что-то в другом месте. Вы случайно не любитель садо-мазо?


  1. komissarex
    25.11.2016 12:31
    +3


  1. DistortNeo
    27.11.2016 05:13

    Только что увидел такой код:

    auto main() -> int
    {
       ...
    }
    Вопрос: зачем?


    1. Ivanbk
      27.11.2016 07:53
      +1

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

      static auto selfTypePtr() ->decltype(this);

      Кстати интересный пример — статический метод использующий this


      1. TheCalligrapher
        28.11.2016 00:21

        Очередная GCC-шная самодеятельность (либо просто дыра). Использование this в объявлениях статических функций-членов запрещено языком. Clang корректно отлавливает эту ошибку. GCC пропускает, даже в режиме -pedantic-errors.


        1. Ivanbk
          28.11.2016 05:23

          А мне не нужен this, мне нужна только декларация. Функция никогда не будет вызываться, у нее даже тела нет. С помощью ее я могу узнать собственный тип там где обычно это невозможно.


          1. TheCalligrapher
            28.11.2016 07:17

            Я понимаю. Однако спецификация языка однозначно утверждает, что this "shall not appear within the declaration of a static member function", даже несмотря на то, что "its type and value category is defined within a static member function as it is within a non-static member function".


            1. Ivanbk
              28.11.2016 08:33

              Вот это явная дыра:

              template<int value>
              struct B {
              	enum {VALUE = value};
              };
              struct A {
              	int v1;
              	int v2;
              	static auto offset() -> B<(int)&this->v2 - (int)&this->v1>;
              };
              ...
              decltype(A::offset())::VALUE; //Что вернет такое выражение?
              


              1. Antervis
                28.11.2016 09:00

                ошибку компиляции вестимо


                1. Ivanbk
                  28.11.2016 09:14

                  В том то и дело, что нет, но результат всегда будет 0.


                  1. TheCalligrapher
                    28.11.2016 09:19

                    Это в каком это компиляторе, интересно?


                    GCC:
                    error: 'this' is not a constant expression


                    Clang:
                    error: non-type template argument is not a constant expression
                    note: use of 'this' pointer is only allowed within the evaluation of a call to a 'constexpr' member function


                    1. Ivanbk
                      28.11.2016 09:52

                      ну, в частности у меня gcc version 4.9.2 (AVR_8_bit_GNU_Toolchain_3.5.3_1700) на других архитектурах и версиях не проверял


    1. TheCalligrapher
      28.11.2016 00:11

      Ответ: а почему бы и нет?


    1. Antervis
      28.11.2016 06:12

      во многих других языках тип возвращаемого значения в функции указывается в конце объявления.