Блог пользователя VC15

Автор VC15, 12 лет назад, По-русски

C++0x наконец-то вышел. Кто-то безгранично рад этому и уже по полной использует его новые возможности, кому-то все равно, ну а кто-то не видит в этом ничего хорошего. Что касается меня, то я отношусь к нему с осторожностью, изучая его и пытаясь найти рациональное применение новым особенностям языка.

Вчера я смотрел доклад Герба Саттера на конференции Going Native 2012, где он рассказывал о С++11 и о том, как он меняет способ использования С++. Рассказывая о ключевом слове auto, Герб призвал использовать его по умолчанию, явно указывая тип только когда это необходимо. Бесспорно, использование auto сильно упрощает набор олимпиадного кода. Например,

map<string, pair<int, int> > m;
auto i = m.find("a");

писать гораздо проще, чем "по-старому".

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

auto x = foo(Widget());
x.bar();

Что можно сказать, взглянув на этот код? Очевидно, функция foo() возвращает объект некоторого класса, у которого потом вызывается метод bar(). Для дальнейшего использования объекта x мы должны знать его класс, чтобы обращаться к его интерфейсу. Как узнать этот класс?

Ответ напрашивается сам собой — посмотреть объявление функции foo(). Любая современная IDE подскажет вам тип возвращаемого ею значения, вам даже не придётся искать её в проекте. Однако это требует выполнения дополнительного действия, в котором не было бы необходимости при явном указании типа. К тому же, стоит отметь, что возникает зависимость от IDE, с бумажки такой код читать будет тяжелее. Пусть это и не столь критично, но всё-таки...

А ведь ситуация может оказаться ещё веселее. Рассмотрим вот такой код:

auto foo(Widget w) -> decltype(w.prop())
{
	return w.prop();
}

Что мы здесь видим? Тип функции foo() нигде явно не определён, необходимо узнать, что возвращает метод prop() класса Widget. А это требует дальнейшего исследования кода. В результате мы можем уйти глубоко в дебри нашего проекта, только чтобы узнать, какой тип имеет переменная x. Конечно, приведённые примеры искусственные, но мне кажется, нечто подобное запросто может появиться и в реальном коде.

Резюме. Этой статьёй я не хочу сказать, что C++11 — это плохо, и его использование не принесёт ничего кроме дополнительной головной боли. Я только хочу сказать, что использовать его возможности надо умеренно и только там, где они действительно упрощают создание и понимание кода. auto, на мой взгляд, — это очень полезная возможность C++11, однако его чрезмерное употребление может существенно усложнить понимание написанного кода. Поэтому я не согласен с Саттером и считаю, что использование auto по умолчанию — не очень хорошая идея.

  • Проголосовать: нравится
  • +53
  • Проголосовать: не нравится

»
12 лет назад, # |
Rev. 2   Проголосовать: нравится +4 Проголосовать: не нравится

Я согласен с автором. У меня возникли такие же мысли, когда я читал про использование var в C#, где оно означает то же, что auto в С++11. Вот что пишут про var:

You should use implicitly typed variable declarations sparingly. Obviously, for anonymous types, it is not possible to specify the data type, and the use of var is required. However, for cases where the data type is not an anonymous type, it is frequently preferable to use the explicit data type. As is the case generally, you should focus on making the semantics of the code more readable while at the same time using the compiler to verify that the resultant variable is of the type you expect. To accomplish this with implicitly typed local variables, use them only when the type assigned to the implicitly typed variable is entirely obvious. For example, in var items = new Dictionary<string, List>();, the resultant code is more succinct and readable. In contrast, when the type is not obvious, such as when a method return is assigned, developers should favor an explicit variable type declaration such as the following: Dictionary<string, List> dictionary = GetAccounts();

Mark Michaelis "Essential C# 4.0"

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

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

  • »
    »
    12 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится +5 Проголосовать: не нравится

    По поводу примера Dictionary<string, List> dictionary = GetAccounts();

    Обычно в больших проектах не рекомендуется использовать Dictionary<string, List> as is. Если этот Dictionary используется много раз, то разумно завести typedef на него.

    typedef Dictionary<string, List> TAccounts;
    

    Также не стоит называть переменную dictionary -- так как это не отражает ее семантику. Я бы называл переменную accounts.

    После этого приведенный кусок (до С++11) будет выглядеть следующим образом

    TAccounts accounts = GetAccounts();
    

    Очевидно, что от применения auto код только выиграет (потому что и так понятно о чем речь).

    auto accounts = GetAccounts();
    

    Мое мнение, что если придерживаться грамотного code-style (например, не называть переменную ничего не значащим именем x), то от auto чиаемость кода только повышается. (Так как избавляет от тавтологий и страшных крокодилов)

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится +1 Проголосовать: не нравится

      Мне кажется, что TAccounts accounts = GetAccounts(); ничем не хуже чем auto accounts = GetAccounts(); .

      Т.е. на самом деле первый вариант лучше, учитывая аргументы, приведенные в оригинальном посте.

»
12 лет назад, # |
Rev. 2   Проголосовать: нравится +33 Проголосовать: не нравится

По-моему, с читаб**Е**льностью ничего не произойдёт в том случае, если использовать auto не всюду и повально, а только в тех местах, где тип выражения можно явно зрительно вывести из окружения и это действительно резко сокращает код. Например, чтобы проитерироваться по контейнеру. Меня жутко порадовала возможность писать for (auto y : E[x]) вместо for (list<int>::iterator it = E[x].begin(); it != E[x].end(); it++) без нагромождения макросов. Ясно же, что y должно иметь тип содержимого контейнера E[x]? А писать auto x = 4 + y * z по мне глупо.

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Согласен. Вопрос в том, почему тогда Саттер рекомендует использовать auto где только можно?

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

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

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Согласен, это один из примеров подходящего применения auto. Вот другие примеры, в которых его применение мне кажется уместным:

    // Один из очень распространённых примеров
    auto func = [](int i, int j) { return i < j; }
    
    // Здесь i имеет именно тот тип, который используется в std::vector для индексации
    std::vector<int> v;
    for (auto i = v.size() - 1; i >= 0; --i)
    {
        v[i] = i * i;
    }
    
    // Ну и, конечно же, шаблоны
    template <class T> void f(const T& inst)
    {
        auto x = inst.bar(); 
        while (x != T::SOME_VALUE) // x может быть любым типом, для которого определён оператор !=
        {
            x = foo(x);
        }
    }
    
    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится +5 Проголосовать: не нравится

      Я бы не сказал, что такое применение, как в примере с вектором, можно назвать уместным.

      Это скорее пример, как с помощью auto сделать работающий код неработающим. :)

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        Да, согласен, пример не удался.

        Я правильно понимаю, что "сделать работающий код неработающим" относится к ситуации, когда v.size() == 0, потому что в этом случае i будет проинициализировано большим положительным числом?

        • »
          »
          »
          »
          »
          12 лет назад, # ^ |
            Проголосовать: нравится 0 Проголосовать: не нравится

          А также к условию i >= 0, которое всегда выполняется.

          • »
            »
            »
            »
            »
            »
            12 лет назад, # ^ |
            Rev. 3   Проголосовать: нравится 0 Проголосовать: не нравится

            Ну да, поторопился я с этим примером. Добиться типобезопасности при итерации по вектору, видимо, можно вот так:

            for (decltype(v.size()) i = 0; i < v.size(); ++i)
            {
               v[i] = i * i;
            }
            

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

            • »
              »
              »
              »
              »
              »
              »
              12 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится

              size_t — по стандарту.

              • »
                »
                »
                »
                »
                »
                »
                »
                12 лет назад, # ^ |
                  Проголосовать: нравится 0 Проголосовать: не нравится

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

                Ну а во-вторых, если заменить std::vector на какой-нибудь другой вектор (например, самописный или QVector из библиотеки Qt), то использование decltype гораздо предпочтительнее, чем запоминание типа для каждого варианта вектора :)

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  12 лет назад, # ^ |
                  Rev. 3   Проголосовать: нравится 0 Проголосовать: не нравится

                  Во-первых, size_t это implementation defined. Во-вторых, и у std::vector, и у QVector есть size_type.

                  PS: А для пробегания в обратном порядке можно воспользоваться boost::adaptors::reverse

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  12 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  Я никоим образом не пытаюсь сказать, что Вы неправы. Всё, о чём вы говорите, безусловно, верно.

                  Однако концептуальный вес всего того, о чём Вы упомянули (зависимость size_t от реализации, наличие vector::size_type и boost::adaptors::reverse), явно больше, чем простое использование decltype. Ведь достаточно одного беглого взгляда на цикл с его использованием, чтобы понять, что он перебирает элементы вектора. В то же время для понимания конструкции

                  std::vector<int> vec;
                  boost::copy( vec | boost::adaptors::reversed,
                               std::ostream_iterator<int>(std::cout) );
                  

                  требуется немало дополнительных знаний.

                  С другой стороны, стоит отметить, что сама по себе эта конструкция неплоха, так как она надёжна и менее подвержена ошибкам по сравнению с обычным циклом (об этом говорит Страуструп в своём докладе: "используйте стандартные алгоритмы вместо аналогичного самописного кода")

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      for(auto i=v.size();i--;)

»
12 лет назад, # |
  Проголосовать: нравится +3 Проголосовать: не нравится

В питоне вообще нет никаких объявлений и, тем не менее, это едва ли не самый читабельный язык на свете.

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится +2 Проголосовать: не нравится

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

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится -7 Проголосовать: не нравится

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

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится +4 Проголосовать: не нравится

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

        Понятно, что причин этому может быть несколько:

        • моя собственная убогость;

        • отсутствие у меня опыта работы с питоном, и привычка к С-образным языкам;

        • качество кода, с которым мне пришлось столкнуться;

        • собственно сам питон.

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

        В этом плане, повсеместное использование auto, вероятно, так же плохо.

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится +3 Проголосовать: не нравится

        У меня нет опыта работы с Python'ом, я почти не изучал этот язык, поэтому не могу оценивать его. Однако код, который мне доводилось видеть, был очень похож на магические письмена.

        Я думаю, было бы очень хорошо, если бы Вы привели пару примеров, демонстрирующих наглядность и читабельность Python'а.

        • »
          »
          »
          »
          »
          12 лет назад, # ^ |
            Проголосовать: нравится +1 Проголосовать: не нравится

          Чтобы спокойно читать питон надо, естественно, его знать и понимать. Он сильно отличается идеалогически от какого-нибудь С++. Например, функциональный стиль с непривычки многих вышибает в осадок, хотя ничего в нем страшного нет. Или тот же duck typing, надо к нему привыкнуть и не впадать в панику постоянно пытаяся выяснить что же за конкретный тип у переменной. А так, если не знать, то конечно можно долго рассуждать что китайский язык плохой потому что в нем черточки вместо букв.

  • »
    »
    12 лет назад, # ^ |
    Rev. 5   Проголосовать: нравится +3 Проголосовать: не нравится

    Читабельность — во-первых, понятие очень субъективное, во-вторых зависит больше от того, кто пишет, чем от языка, на котором он пишет. Я вон даже на perl-е встречал проекты с вполне читаемым кодом. А вот, что делает следующий код на втором питоне, с первого взгляда сказать не могу:

    [
                          globals()
                    .update({   "______":
                 lambda x:         globals()
                 .update((         dict([[x]
                    *2])))}),   ______(((
                          "Just")))
                    ,______((   "another"
                 )),______         ("Python"
              ),______(               "Hacker")
              ];print (               " ".join(
                 [(Just),(         (another)
                    ),(Python   ),Hacker]
                          ));______
    

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

    #Sometimes_code_does_not_really_need_to_have_syntacally_significant_whitespace
    g,u,n=range(3),"Why don't I use Java or Perl?",lambda(x),y:reduce(x.__add__,y)
    print"".join([u[P:H+P]for(H,P)in(zip(n(list,n(tuple,[([d],n(list,[[1+c%2]*(c+1
    )for(c)in(g)]))for(d)in(g)])[1:]),[16,12,8,19,6,5,8,1,25,23,2,8,1,5,14,25]))])
    
    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

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

      А так я и говорю что мне довелось покопаться в чужом коде на разных языках. Питон мне (субъективно) понравился больше всех, хотя С# тоже неплох.

»
12 лет назад, # |
  Проголосовать: нравится +5 Проголосовать: не нравится

По хорошему среда должна подсказать тип i при наведении на нее мышкой.

Я согласен с тем, что использовать auto либо когда код сокращается очень сильно, либо когда его избежать нельзя.

Не могу не отметить еще, что фраза "я не согласен с Саттером" в вопросах, связанных с С++, звучит забавно :о)

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Абсолютно согласен. Но, к сожалению, среды, в которых мне приходится работать (Eclipse и QtCreator), пока не умеют делать такие подсказки.