В комментариях к переводу «30 лет С++» было заметно, что далеко не все этапы эволюции языка одинаково хорошо известны, иногда вообще нету представления о происхождении и процессе развития того или иного элемента синтаксиса или соответствующей семантики. Возможно, этой заметкой удастся заинтересовать читателей обратится к уже давно не новой книге автора языка с целью формирования более полной картины о C++. Книга рассказывает как происходило его развитие, что оказывало влияние на этот процесс и почему было отдано предпочтение одним подходам вместо других.

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

Часть материала несколько пересекается с недавними постами, но не была вырезана для полноты. Порядок может показаться сумбурным, но на то это и выборка, дополнительная причина «беспорядка» обозначена в заключении. При чтении полезно иметь в виду, что первое издание книги вышло в начале 1994 года.

Выдержки


  1. Всё началось в 1971 году, когда Бъёрну Страуструпу понадобился быстрый язык для выполнения моделирования.

  2. Классы пришли в первую очередь из Simula.

  3. const-квалификаторы обязаны своим существованием ROM.

  4. Деннис Ритчи принимал активное участие в обсуждении дизайна C++. (Ниже будет видно, как сильно C и C++ влияли друг на друга.)

  5. «Изначально в языке предлагались общие механизмы организации программ, а вовсе не поддержка конкретных предметных областей», т.е. мультипарадигменность у C++ врождённая и соответствует задумке его автора.

  6. «Я искренне убеждён, что не существует единственно правильного способа написать программу, и дизайнер языка не должен заставлять программиста следовать определённому стилю.»

  7. Первоначальная реализация не поддерживала виртуальные функции.

  8. Множественное наследование было добавлено спустя годы после начала разработки языка.

  9. Изначально конструкторы классов назывались new и возвращали void (хотя его можно было опускать). При этом именно конструкторы выделяли память под объект, а деструкторы освобождали её:
    class stack
    {
        void new();
        // new();
    };
    

  10. Исходным именем деструкторов было delete и они тоже возвращали void (хотя и его можно было опускать):
    class stack
    {
        void delete();
        // delete();
    };
    

  11. Вместо двойного двоеточия (::) для объявления методов классов использовалась точка:
    char stack.pop()
    {
        // ...
    }
    

  12. Имена классов пребывали в отдельном пространстве имён и должны были предваряться ключевым словом class (также как и struct/union в C):
    class stack stack;
    class stack *thatStack = &stack;
    

  13. Само ключевое слово class пришло из Simula, а так как Бьёрн не любитель изобретать терминологию то оставил то, к чему привык.

  14. Язык Simula позволял создавать экземпляры классов только на куче, что было крайне неудобным и подало идею позволить создавать их в C++ на стеке либо глобально.

  15. «C++ — это просто ещё один язык в системе, а не вся система.»

  16. В первой реализации не существовало возможности получить доступ к this, но это было в большей степени следствием ошибки.

  17. Автопрототипирование (вывод и запоминание прототипа неизвестной функции в месте первого обращения к ней) изначально было придумано для C++, но позже было использовано в C (хотя и объявлено устаревшим сейчас):
    function(1, 1.0);   // Компилятор запоминает, что function принимает int и double.
    function("string"); // Компилятор выдаёт ошибку из-за несоответствия типов аргументов int и double.
    

  18. Объявление функции, не принимающей аргументы, с использованием (void) было добавлено сначала в C++, а только потом в C (тогда в C++ от этой идеи уже отказались и решили использовать пустые круглые скобки):
    void noArgsInCpp();
    void noArgsInCAndCpp(void);
    

  19. Объявление неявного int в качестве устаревшего было изначально предложено для C++:
    function() {} // Компилятор предполагает, что function возвращает int.
    

  20. В ранние периоды предпринимались попытки запретить сужающие преобразования (long -> int, double -> int), но из-за слишком большой распространённости было решено отказаться от этой идеи:
    void f(long lng, int i)
    {
        i = lng;    // Вызвало бы ошибку.
        char c = i; // Вызвало бы ошибку.
    }
    

  21. Перегрузка операторов, ссылки и возможность объявлять переменные в любом месте блока пришли из ALGOl 68.

  22. Однострочные комментарии (//) — из BCPL.

  23. Источники, использованные при разработке исключений: Ada, Clu, ML.

  24. Шаблоны и пространства имён позаимствованы из языка Ada.

  25. Указание типов параметров в объявлении функции — впервые реализовано в C++, позже адаптировано в C.

  26. Рассматривалась возможность введения альтернативного синтаксиса объявлений (недавно был пост по близкой теме):
    // Радикальный вариант:
    v: [10]->int; // int *v[10];
    p: ->[10]int; // int (*p)[10];
    
    // Менее радикальный вариант:
    int v[10]->; // int *v[10];
    int p->[10]; // int (*p)[10];
    int f(char)->[10]->(double)->; // int *(*(*f(char))[10])(double);
    
    Новый синтаксис не был проработан во всех деталях и в язык он так и не попал ввиду небольшой значимости изменения на фоне потенциально больших проблем с обратной совместимостью.

  27. Отказ от обязательных префиксов типов (struct/union/class) привёл к возможности объявления переменных, совпадающих по имени с классами (по большому счёту ради совместимости с C):
    class Class
    {
    };
    
    int main()
    {
        Class Class;
        return 0;
    }
    

  28. Одно время разрешалось объявление новых составных типов в списке аргументов или же прямо в возвращаемом значении функции:
    class A
    {
        // ...
    } get()
    {
        return A();
    }
    

  29. Ранее «вытягивание» членов базовых классов не требовало использования ключевого слова using, достаточно было просто указать имя члена:
    class Base
    {
    protected:
        void doSomething();
        void doSomethingElse();
    };
    
    class Derived : public Base
    {
    public:
        doSomething; // using doSomething;
        Base.doSomethingElse; // using doSomethingElse;
    };
    

  30. Изначально, дружественными (friend) могли быть только классы. Общей идеей концепции является помещение ряда сущностей в один домен безопасности, члены которого равны между собой в правах (а не нарушение инкапсуляции, как может сложиться впечатление, что, впрочем, не отменяет возможность такого использования).

  31. Возможно, именно Страуструп изобрёл концепцию конструктора.

  32. Изначально, каждый объект мог содержать методы call() и return(), которые вызывались соответственно перед и после выполнения любого метода класса. Аналогичные методы :before и :after есть в CLOS.
    class ProtectedAccess : object
    {
        call();   // Захватить примитив синхронизации.
        return(); // Освободить примитив.
    };
    
    Позже решили, что эта возможность добавляет больше трудностей в язык, чем приносит пользы.

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

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

  35. Какое-то время язык назывался C84 для избежания путаницы, так как пользователи замещали C with classes чем-то вроде «новый C», «улучшенный C» и т.д. Оптимистично считая, что C будет стандартизирован в 1985, Бьёрна попросили поменять название ещё раз, во избежания возможной неоднозначности со «стандартным C».

  36. Первый же компилятор C++ (не препроцессоры, которые использовались изначально) был написан на C++ (Cfront, C with classes и C84, вроде, были написаны на C).

  37. Вероятно, Cfront был первым компилятором неполного цикла, генерирующим код на C. За ним последовали Ada, Eiffel, Lisp, Modula-3, Smalltalk.

  38. Первая реализация исключений была добавлена Hewlett-Packart в 1992 году.

  39. Виртуальные функции были заимствованы из Simula, но с модификациями.

  40. Определение типа во время исполнения первоначально не было добавлено намеренно, чтобы заставить пользователей применять статический контроль типов и виртуальные функции, а не run-time тип, который по своей сути — switch.

  41. struct почти эквивалентно class с целью единообразия и унификации.

  42. До Cfront 2.0 operator= мог быть глобальной функцией, позже отказались от этой идеи из-за конфликтующей предопределённой семантики.
    class C {};
    
    C & operator=(C &rhs) {}
    
    // Приводит к ошибке:
    // src.cpp:3:21: error: ‘C& operator=(C&)’ must be a nonstatic member function
    

  43. Определение переменной в месте её использования взято из ALGOL 68.

  44. Ссылки также заимствованы из ALGOL 68, но там они могли быть переприсвоены после инициализации.

  45. Перегрузка по lvalue/rvalue рассматривалась ещё во времена Cfront 1.0.

  46. readonly/writeonly указатели/данные были придуманы Страуструпом и Деннисом Ритчи в 1981 году, а позже были добавлены в стандарт C комитетом ANSI C (X3J11), но в несколько урезанном виде: только readonly и после его переименования в const.

  47. Ключевое слово new также взято из Simula.

  48. Первоначальная реализация размещения объектов предполагала присваивание this в конструкторе.

  49. constructor/destructor в качестве имён соответствующих специальных методов были отвергнуты для уменьшения количества ключевых слов, а также для более очевидного синтаксиса.

  50. Функции new() и delete() (старые имена конструктора и деструктора) в C with classes по умолчанию автоматически получали модификатор доступа public.

  51. Оператор :: был введён для разрешения неоднозначности с точкой.

  52. Изначально, переменные, введённые в списке инициализации for, были видны после его тела (из-за неточной формулировки «имена видны от места объявления до конца области»). Многие, наверное, сталкивались с этим в Borland C++.
    for (int i = 0; i < n; ++i) {
        // ...
    }
    if (i >= n) { // То же i, что объявлено в for.
    

  53. Вложенные области видимости классов были добавлены, позже убраны для совместимости с C и вновь добавлены ещё раз.
    // Этот код работает и в C и в C++:
    struct Outer
    {
        struct Inner { };
    };
    // Но в C он эквивалентен следующему:
    struct Outer { };
    struct Inner { };
    

  54. static по умолчанию для глобальных символов противоречил правилам C и по этой причине не был добавлен в C++.

  55. Строгий контроль типов при вызове функций в C пришёл из C++.

  56. «Ключ к хорошему дизайну — глубокое понимание стоящих перед языком задач, а не включение самых передовых идей.»

  57. Абстрактные классы, типобезопасная компоновка и множественное наследование появились в версии Cfront 2.0.

  58. Произвольный порядок объявлений элементов класса доставил проблем и привел к некоторому неравноправию между типами и данными. Так делать разрешено:
    int x;
    class X
    {
        int f() { return x; } // x означает X::x
        int x;
    };
    
    А так уже запрещено:
    typedef char *T;
    class Y
    {
        T f() { T a = 0; return a; } // Ошибка, т.к. T меняет своё значение на следующей строке.
        typedef f int T;
    };
    
    Это же справедливо и для типов функций, ибо:
    typedef int P();
    class X
    {
        static P(Q1); // static int Q1();
        static P Q2;  // static int Q2();
    };
    

  59. В Cfront временные объекты уничтожались в конце блока (сейчас — после вычисления выражения, если временный объект не привязывается):
    const char *p;
    std::string s1 = ..., s2 = ...;
    if ((p = (s1 + s2).c_str()) && p[0]) {
        // Здесь можно было работать с p.
        // ...
        // Тут уничтожался временны объект, созданный выражением "s1 + s2".
    }
    

  60. Может ещё для кого-то будет новостью, но у Pascal также есть ISO/IEC стандарты (просто таких языков достаточно мало).

  61. Предложение о добавлении именованных аргументов было рассмотрено, но отклонено. (При чтении книги создалось впечатление, что отклонялось довольно большое количество различных предложений; впрочем, многое из этого всё же попало в стандарт спустя десятилетия.)

  62. restrict (noalias) указатели так и не были адаптированы из C несмотря на то, что были известны в начале 90-х.

  63. Специальные символы вызвали проблемы распространения C в Европе из-за широкого использования 7-битных кодировок. Отсюда взялись триграфы, диграфы и специальные слова (and, or и т.д.; см.). Всё это перекочевало и в C++.

  64. Бюджет, который AT&T выделила на C++ за всё время, составляет примерно 3000$. Из них 1000$ пошла на рассылку рекламы C++ покупателям UNIX и 2000$ на первую конференцию, на которой всё было более чем скромно (даже на бумагу не хватило, волонтёры делали копии технической документации на бланках регистрации посетителей...). Тем не менее отсутствие какого-либо маркетинга в течении десятилетий не помешало широкому распространению языка.

Заключение


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

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


  1. Hyston
    13.11.2015 16:12
    +1

    Вот черт, а я уже и забыл, что можно and, or и прочие использовать.


  1. FeelUs
    13.11.2015 17:41
    +1

    Вот интересно, почему операторы (кроме operator=() ) можно объявлять (и определять) вне классов, а методы нельзя?


    1. xaizek
      13.11.2015 18:25

      Объявление оператора вне класса позволяет использовать встроенный тип в качестве левого операнда:

      class C { };
      
      int operator+(int a, const C &c) { return 0; }
      
      Это также позволяет выполнять неявные преобразования левого операнда. Скажем иначе это бы не работало:
      std::string s;
      "something" + s;
      

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


      1. GamePad64
        13.11.2015 18:31

        Зато, можно будет расширять существующие классы non-intrusive. Скажем, добавить к std::string метод .toBase64() или что-то вроде того.


        1. xaizek
          13.11.2015 18:38
          +1

          Это да, хорошее в этом есть, я больше боюсь злоупотреблений. При виде std::string("something").toupper() хочется пойти и посмотреть, когда это добавили toupper для std::string, а потом сидеть удивляться, почему его в документации нет, а код работает, найти где же он действительно объявлен тоже будет не очень просто.


          1. GamePad64
            13.11.2015 18:48

            В современных IDE решается через Ctrl+click. Сразу становится видно, где объявлена эта функция.


            1. xaizek
              13.11.2015 19:04
              +2

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


  1. orcy
    13.11.2015 22:14
    +1

    ALGOL 68 видимо крутая штука была


  1. Daniro_San
    14.11.2015 08:50
    +1

    Выделение памяти в куче через конструкторы почему то напомнило Delphi.
    Хорошо, что сейчас объекты классов могут располагаться и в стеке, по сравнению с тем же C#.


  1. ArmanPrestige
    15.11.2015 11:22
    +1

    Спасибо за статью. Узнал много нового о своём любимом языке программирования.

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

    А вот это можно отнести к «священным войнам».


    1. xaizek
      15.11.2015 12:27

      Спасибо за замечание, подправил формулировку на менее «холиварную».