C++

Материал из Википедии — свободной энциклопедии
Перейти к: навигация, поиск
C++
Семантика:

мультипарадигмальный: объектно-ориентированное, обобщённое, процедурное, метапрограммирование

Тип исполнения:

компилируемый

Появился в:

1983

Автор(ы):

Бьярне Страуструп

Расширение файлов:

.c++ .cpp .cxx .cc .h++ .hpp .hxx .hh .h

Система типов:

статическая

Основные реализации:

GNU C++, Microsoft Visual C++, Intel C++ compiler, Open64 C++ Compiler, Clang, Comeau C/C++, Embarcadero C++ Builder, Watcom C++ compiler, Digital Mars C++, Oracle Solaris Studio C++ compiler, Turbo C++

Диалекты:

ISO/IEC 14882 C++

C++ — компилируемый статически типизированный язык программирования общего назначения.

Поддерживает такие парадигмы программирования как процедурное программирование, объектно-ориентированное программирование, обобщённое программирование, обеспечивает модульность, раздельную компиляцию, обработку исключений, абстракцию данных, объявление типов (классов) объектов, виртуальные функции. Стандартная библиотека включает, в том числе, общеупотребительные контейнеры и алгоритмы. C++ сочетает свойства как высокоуровневых, так и низкоуровневых языков.[1] В сравнении с его предшественником — языком C, — наибольшее внимание уделено поддержке объектно-ориентированного и обобщённого программирования.[2]

C++ широко используется для разработки программного обеспечения, являясь одним из самых популярных языков программирования[мнения 1][мнения 2]. Область его применения включает создание операционных систем, разнообразных прикладных программ, драйверов устройств, приложений для встраиваемых систем, высокопроизводительных серверов, а также развлекательных приложений (игр). Существует множество реализаций языка C++, как бесплатных, так и коммерческих и для различных платформ. Например, на платформе x86 это GCC, Visual C++, Intel C++ Compiler, Embarcadero (Borland) C++ Builder и другие. C++ оказал огромное влияние на другие языки программирования, в первую очередь на Java и C#.

Синтаксис C++ унаследован от языка C. Одним из принципов разработки было сохранение совместимости с C. Тем не менее, C++ не является в строгом смысле надмножеством C; множество программ, которые могут одинаково успешно транслироваться как компиляторами C, так и компиляторами C++, довольно велико, но не включает все возможные программы на C.

Содержание

История[править | править исходный текст]

Исторический этап развития[3] Год
Язык BCPL 1966
Язык Би (оригинальная разработка Томпсона под UNIX) 1969
Язык Си 1972
Си с классами 1980
C84 1984
Cfront (выпуск E) 1984
Cfront (выпуск 1.0) 1985
Множественное/виртуальное наследование 1988
Обобщённое программирование (шаблоны) 1991
ANSI C++ / ISO-C++ 1996
ISO/IEC 14882:1998 1998
ISO/IEC 14882:2003 2003
C++/CLI 2005
TR1 2005
C++11 2011

История создания[править | править исходный текст]

Язык возник в начале 1980-х годов, когда сотрудник фирмы Bell Labs Бьёрн Страуструп придумал ряд усовершенствований к языку C под собственные нужды. [4] Когда в конце 1970-х годов Страуструп начал работать в Bell Labs над задачами теории очередей (в приложении к моделированию телефонных вызовов), он обнаружил, что попытки применения существующих в то время языков моделирования оказываются неэффективными, а применение высокоэффективных машинных языков слишком сложно из-за их ограниченной выразительности. Так, язык Симула имеет такие возможности, которые были бы очень полезны для разработки большого программного обеспечения, но работает слишком медленно, а язык BCPL достаточно быстр, но слишком близок к языкам низкого уровня и не подходит для разработки большого программного обеспечения.

Вспомнив опыт своей диссертации, Страуструп решил дополнить язык C (преемник BCPL) возможностями, имеющимися в языке Симула. Язык C, будучи базовым языком системы UNIX, на которой работали компьютеры Bell, является быстрым, многофункциональным и переносимым. Страуструп добавил к нему возможность работы с классами и объектами. В результате практические задачи моделирования оказались доступными для решения как с точки зрения времени разработки (благодаря использованию Симула-подобных классов), так и с точки зрения времени вычислений (благодаря быстродействию C). В первую очередь в C были добавлены классы (с инкапсуляцией), наследование классов, строгая проверка типов, inline-функции и аргументы по умолчанию. Ранние версии языка, первоначально именовавшегося «C with classes» («Си с классами»), стали доступны с 1980 года.

Разрабатывая C с классами, Страуструп написал программу cfront[en]транслятор, перерабатывающий исходный код C с классами в исходный код простого C. Это позволило работать над новым языком и использовать его на практике, применяя уже имеющуюся в UNIX инфраструктуру для разработки на C. Новый язык, неожиданно для автора, приобрёл большую популярность среди коллег и вскоре Страуструп уже не мог лично поддерживать его, отвечая на тысячи вопросов.

При создании C++ Бьёрн Страуструп хотел [5]

  • Получить универсальный язык со статическими типами данных, эффективностью и переносимостью языка C.
  • Непосредственно и всесторонне поддерживать множество стилей программирования, в том числе процедурное программирование, абстракцию данных, объектно-ориентированное программирование и обобщённое программирование.
  • Дать программисту свободу выбора, даже если это даст ему возможность выбирать неправильно.
  • Максимально сохранить совместимость с C, тем самым делая возможным лёгкий переход от программирования на C.
  • Избежать разночтений между C и C++: любая конструкция, допустимая в обоих языках, должна в каждом из них обозначать одно и то же и приводить к одному и тому же поведению программы.
  • Избегать особенностей, которые зависят от платформы или не являются универсальными.
  • «Не платить за то, что не используется» — никакое языковое средство не должно приводить к снижению производительности программ, не использующих его.
  • Не требовать слишком усложнённой среды программирования.

Выбор именно C в качестве базы для создания нового языка программирования объясняется тем, что язык C:

  1. является многоцелевым, лаконичным и относительно низкоуровневым языком;
  2. подходит для решения большинства системных задач;
  3. исполняется везде и на всём;
  4. стыкуется со средой программирования UNIX.

— Б. Страуструп. Язык программирования C++. Раздел 1.6[6]

Несмотря на ряд известных недостатков языка C, Страуструп пошёл на его использование в качестве основы, так как «в C есть свои проблемы, но их имел бы и разработанный с нуля язык, а проблемы C нам известны». Кроме того, это позволило быстро получить прототип компилятора (cfront), который лишь выполнял трансляцию добавленных синтаксических элементов в оригинальный язык C.

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

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

К 1983 году в язык были добавлены новые возможности, такие как виртуальные функции, перегрузка функций и операторов, ссылки, константы, пользовательский контроль над управлением свободной памятью, улучшенная проверка типов и новый стиль комментариев (//). Получившийся язык уже перестал быть просто дополненной версией классического C и был переименован из C с классами в «C++». Его первый коммерческий выпуск состоялся в октябре 1985 года.

Имя языка, получившееся в итоге, происходит от оператора унарного постфиксного инкремента C ++ (увеличение значения переменной на единицу).[7]

До начала официальной стандартизации язык развивался в основном силами Страуструпа в ответ на запросы программистского сообщества. Функцию стандартных описаний языка выполняли написанные Страуструпом печатные работы по C++ (описание языка, справочное руководство и так далее).

История стандартов[править | править исходный текст]

В 1985 году вышло первое издание «Языка программирования C++», обеспечивающее первое описание этого языка, что было чрезвычайно важно из-за отсутствия официального стандарта. В 1989 году состоялся выход C++ версии 2.0. Его новые возможности включали множественное наследование, абстрактные классы, статические функции-члены, функции-константы и защищённые члены. В 1990 году вышло «Комментированное справочное руководство по C++», положенное впоследствии в основу стандарта. Последние обновления включали шаблоны, исключения, пространства имён, новые способы приведения типов и булевский тип.

Стандартная библиотека C++ также развивалась вместе с ним. Первым добавлением к стандартной библиотеке C++ стали потоки ввода-вывода, обеспечивающие средства для замены традиционных функций C printf и scanf. Позднее самым значительным развитием стандартной библиотеки стало включение в неё Стандартной библиотеки шаблонов.

В 1998 году был опубликован стандарт языка ISO/IEC 14882:1998 (известный как C++98),[8] разработанный комитетом по стандартизации C++ (ISO/IEC JTC1/SC22/WG21 working group). Стандарт C++ не описывает способы именования объектов, некоторые детали обработки исключений и другие возможности, связанные с деталями реализации, что делает несовместимым объектный код, созданный различными компиляторами. Однако для этого третьими лицами создано множество стандартов для конкретных архитектур и операционных систем.

В 2003 году был опубликован стандарт языка ISO/IEC 14882:2003, где были исправлены выявленные ошибки и недочёты предыдущей версии стандарта.

В 2005 году был выпущен отчёт Library Technical Report 1 (кратко называемый TR1). Не являясь официально частью стандарта, отчёт описывает расширения стандартной библиотеки, которые, как ожидалось авторами, должны быть включены в следующую версию языка C++. Степень поддержки TR1 улучшается почти во всех поддерживаемых компиляторах языка C++.

С 2009 года велась работа по обновлению предыдущего стандарта, предварительной версией нового стандарта сперва был C++09, а спустя год C++0x, сегодня — C++11, куда были включены дополнения в ядро языка и расширение стандартной библиотеки, в том числе большую часть TR1.

C++ продолжает развиваться, чтобы отвечать современным требованиям. Одна из групп, разрабатывающих язык C++ и направляющих комитету по стандартизации C++ предложения по его улучшению — это Boost, которая занимается, в том числе, совершенствованием возможностей языка путём добавления в него особенностей метапрограммирования.

Никто не обладает правами на язык C++, он является свободным. Однако сам документ стандарта языка (за исключением черновиков) не доступен бесплатно.

Обзор языка[править | править исходный текст]

Стандарт C++ на 2003 год состоит из двух основных частей: описание ядра языка и описание стандартной библиотеки.

Кроме того, существует огромное количество библиотек C++, не входящих в стандарт. В программах на C++ можно использовать многие библиотеки C.

Стандартизация определила язык программирования C++, однако за этим названием могут скрываться также неполные, ограниченные, достандартные варианты языка. Первое время язык развивался вне формальных рамок, спонтанно, по мере встававших перед ним задач. Развитию языка сопутствовало развитие кросс-компилятора cfront. Новшества в языке отражались в изменении номера версии кросс-компилятора. Эти номера версий кросс-компилятора распространялись и на сам язык, но применительно к настоящему времени речь о версиях языка C++ не ведут.

Необъектно-ориентированные возможности[править | править исходный текст]

В этом разделе описываются возможности, непосредственно не связанные с объектно-ориентированным программированием (ООП), но многие из них, однако, особенно важны в сочетании с ООП.

Комментарии[править | править исходный текст]

С++ поддерживает как комментарии в стиле C:

 /*
 это комментарий, который может состоять
 из нескольких строчек
 */

так и однострочные:

 // вся оставшаяся часть строки является комментарием

где // обозначает начало комментария, а ближайший последующий символ новой строки, который не предварён символом \ (либо эквивалентным ему обозначением ??/), считается окончанием комментария.

Типы[править | править исходный текст]

В C++ доступны следующие встроенные типы:

  • Символьные: char, wchar_t (char16_t и char32_t, в стандарте C++11).
  • Целочисленные знаковые: signed char, short int, int, long intlong long int, в стандарте C++11).
  • Целочисленные беззнаковые: unsigned char, unsigned short int, unsigned int, unsigned long intunsigned long long int, в стандарте C++11).
  • С плавающей точкой: float, double, long double.
  • Логический: bool, имеющий значения true и false.

Операции сравнения возвращают тип bool. Выражения в скобках после if, while приводятся к типу bool.[9]

Функции могут принимать аргументы по ссылке. Например, функция void f(int &x) {x=3;} присваивает своему аргументу значение 3. Функции также могут возвращать результат по ссылке, и ссылки могут быть вне всякой связи с функциями. Например, {double &b=a[3]; b=sin(b);} эквивалентно a[3]=sin(a[3]);. При программировании ссылки в определённой степени сходны с указателями, со следующими особенностями: перед использованием ссылка должна быть инициализирована; ссылка пожизненно указывает на один и тот же адрес; в выражении ссылка обозначает непосредственно тот объект или ту функцию, на которую она указывает, обращение же к объекту или функции через указатель требует разыменование указателя. Существуют и другие отличия в использовании указателей и ссылок. Концептуально ссылка — другое имя переменной или функции, другое название одного и того же адреса, существует лишь только в тексте программы, заменяемое адресом при компиляции; а указатель — переменная, хранящая адрес, к которому обращаются.

Разное[править | править исходный текст]

  • Спецификатор inline позволяет объявлять inline-функции. Функция, определённая внутри тела класса, является inline по умолчанию. Изначально inline-функции задумывались как функции, являющиеся хорошими кандидатами на оптимизацию, при которой в местах обращения к функции компилятор вставит тело этой функции, а не код вызова. В действительности компилятор не обязан реализовывать подстановку тела для inline-функций, но может, исходя из заданных критериев оптимизации, выполнять подстановку тела для функций, которые не объявлены как inline. Пожалуй, наиболее значимой особенностью inline-функции является то, что она может многократно определяться в нескольких единицах трансляции (при этом inline-функция должна быть определена во всех единицах трансляции, где она используется), в то время как функция, не являющаяся inline, может определяться в программе не более одного раза. Пример:

inline double Sqr(double x) {return x*x;}.

  • Описатель volatile используется в описании переменных и информирует компилятор, что значение данной переменной может быть изменено способом, который компилятор не в состоянии отследить. Для переменных, объявленных volatile, компилятор не должен применять средства оптимизации, изменяющие положение переменной в памяти (например, помещающие её в регистр) или полагающиеся на неизменность значения переменной в промежутке между двумя присваиваниями ей значения. В мультиядерной системе volatile помогает избегать барьеров памяти 2-го типа[источник не указан 328 дней].
  • Если описана структура, класс, объединение (union) или перечисление (enum), её имя является именем типа, например:
struct Time {
    int hh, mm, ss;
};
Time t1, t2;
namespace Foo
{
   const int x=5;
   typedef int** T;
   void f(int y) {return y*x};
   double g(T);
   ...
}

то вне фигурных скобок следует обращаться к T, x, f, g как Foo::T, Foo::x, Foo::f и Foo::g соответственно. Если в каком-то файле нужно обратиться к ним непосредственно, можно написать

using namespace Foo;

Или же

using Foo::T;

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

namespace
{
    ...
}

Все имена, описанные в нём, доступны в текущей единице трансляции и больше нигде.

  • Один или несколько последних аргументов функции могут задаваться по умолчанию. К примеру, если функция описана как void f(int x, int y=5, int z=10), вызовы f(1), f(1,5) и f(1,5,10) эквивалентны.
  • При описании функций отсутствие аргументов в скобках означает, в отличие от C, что аргументов нет, а не то, что они неизвестны. Если аргументы неизвестны, надо пользоваться многоточием, например, int printf(const char* fmt, ...).
  • Внутри структуры или класса можно описывать вложенные типы, как через typedef, так и через описание других классов, а также перечислений. Для доступа к таким типам вне класса, к имени типа добавляется имя структуры или класса и два двоеточия:
struct S
{
    typedef int** T;
    T x;
};
S::T y;
  • Могут быть несколько функций с одним и тем же именем, но разными типами или количеством аргументов (перегрузка функций; при этом тип возвращаемого значения на перегрузку не влияет). Например, вполне можно писать:
void Print(int x);
void Print(double x);
void Print(int x, int y);
  • Смысл некоторых операторов применительно к пользовательским типам можно определять через объявление соответствующих операторных функций. К примеру, так:
struct Date {int day, month, year;};
void operator ++(struct Date& date);

Операторные функции во многом схожи с обычными (неоператорными) функциями. За исключением операторов new, new[], delete и delete[], нельзя переопределять поведение операторов для встроенных типов (скажем, переопределять умножение значений типа int); нельзя создавать новые операторы, которых нет в C++ (скажем, **); нельзя менять количество операндов, предусмотренное для оператора, а также нельзя менять существующие приоритеты и ассоциативность операторов (скажем, в выражении a+b*c сначала будет выполняться умножение, а потом сложение, к каким бы типам ни принадлежали a, b и c). Можно переопределить операции [] (с одним параметром) и () (с любым числом параметров).

  • Добавлены шаблоны (template). Например, template<class T> T Min(T x, T y) {return x<y?x:y;} определяет функцию Min для любых типов. Шаблоны могут задавать не только функции, но и типы. Например, template<class T> struct Array{int len; T* val;}; определяет массив значений любого типа, после чего мы можем писать Array<float> x;
  • В дополнение к функциям malloc и free введены операторные функции operator new, operator new[], operator delete и operator delete[], а также операторы new, new[], delete и delete[]. Если T — произвольный объектный тип, не являющийся типом массива, X — произвольный объектный тип и A — тип массива из некоторого количества n элементов, имеющих тип X, то
    • new T выделяет память (посредством вызова функции operator new), достаточную для размещения одного объекта типа Т, возможно, инициализирует объект в этой памяти, и возвращает указатель типа Т* (например, Т* p = new T).
    • new X[n] и new A выделяют память (посредством вызова функции operator new[]), достаточную для размещения n объектов типа X, возможно, инициализируют каждый объект в этой памяти, и возвращают указатель типа X* (например, X* p = new X[n]).
    • delete p — разрушает объект (не являющийся массивом), на который ссылается указатель p, и освобождает область памяти (посредством вызова функции operator delete), ранее выделенную для него new-выражением.
    • delete [] p — разрушает каждый объект в массиве, на который ссылается указатель p, и освобождает область памяти (посредством вызова функции operator delete[]), ранее выделенную для этого массива new-выражением.

Операция delete проверяет, что её аргумент не является нулевым указателем, в противном случае она ничего не делает. Для инициализации объекта non-POD классового типа new-выражение вызывает конструктор; для уничтожения объекта классового типа delete-выражение вызывает деструктор (см. ниже).

Объектно-ориентированные особенности языка[править | править исходный текст]

C++ добавляет к C объектно-ориентированные возможности. Он вводит классы, которые обеспечивают три самых важных свойства ООП: инкапсуляцию, наследование и полиморфизм.

В стандарте C++ под классом (class) подразумевается пользовательский тип, объявленный с использованием одного из ключевых слов class, struct или union, под структурой (structure) подразумевается класс, определённый через ключевое слово struct, и под объединением (union) подразумевается класс, определённый через ключевое слово union.

Описание функций в теле класса[править | править исходный текст]

В теле класса можно указать только заголовок функции, а можно описать всю функцию (см. пример с функцией Alloc ниже. В этом случае она считается встраиваемой (inline))

Константные функции-члены[править | править исходный текст]

Нестатические функции-члены (и только они) могут иметь описатель const

class Array
{
...
    inline double operator[] (int n) const;

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

Наследование[править | править исходный текст]

В C++ при наследовании одного класса от другого наследуется реализация класса, плюс класс-наследник может добавлять свои поля и функции или переопределять функции базового класса. Множественное наследование разрешено.

Конструктор наследника вызывает конструкторы базовых классов, а затем конструкторы нестатических членов-данных, являющихся экземплярами классов. Деструктор работает в обратном порядке.

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

Доступ члена базового класса/режим наследования private-член protected-член public-член
private-наследование недоступен private private
protected-наследование недоступен protected protected
public-наследование недоступен protected public

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

Полиморфизм[править | править исходный текст]

Семантика системы типов С++ не полиморфна (в отличие от потомков ML, в том числе гибридных с Си — BitC, Cyclone), однако есть несколько способов обеспечить полиморфное поведение. Прежде всего это перегрузка методов классов при наследовании — традиционный для ООП способ обеспечения абстракции данных. Затем есть два способа реализации параметрического полиморфизма (в С++-сообществе обычно называемого «обобщённым программированием»). Первый способ унаследован из Си — использование бестипового указателя и приведение типа в зависимости от других данных — хотя в С++ этот способ традиционно считается неидеоматичным и опасным. Второй заключается в использовании шаблонов — но, в отличие от обычных реализаций параметрического полиморфизма, в С++ происходит автоматическая генерация семейства перегруженных мономорфных функций на основании полиморфного определения (шаблона) в соответствии с контекстами его использования — то есть параметрический полиморфизм на уровне исходного кода транслируется в ситуативный (ad hoc) на уровне машинного, за что С++ подвергается критике (см. раздел Вычислительная производительность). В С++ также есть третий вид перегрузки — перегрузка операторов — которая в сочетании с наследованием классов предоставляет ещё большие возможности повышения читабельности кода путём ввода т. н. «синтаксического сахара».

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

class Figure
{
    ...
    void Draw() const;
    ...
};
 
class Square : public Figure
{
    ...
    void Draw() const;
    ...
};
 
class Circle : public Figure
{
    ...
    void Draw() const;
    ...
};
 
class Window
{
    ...
    void Draw() const;
    ...
};
 
class SquareWindow : public Window
{
    ...
    void Draw() const;
    ...
};
 
class RoundWindow : public Window
{
    ...
    void Draw() const;
    ...
};

В результате компиляции этих определений формируется шесть тел функций. В коде они используются одинаково; выбор конкретного экземляра функции осуществляется в зависимости от типа экземпляра объекта, для которого осуществляется вызов. Согласованность поведения функций остаётся на совести программиста.

Circle *c = new Circle(0,0,5);
Figure *f = c; // правильно: Figure — базовый класс для Circle
c->Draw();
f->Draw(); // Указатели равны друг другу, но для f и c будут вызваны разные функции
SquareWindow *sw = new SquareWindow(0,0,5);
sw->Draw(); // используется так же
f = sw; // ошибка! SquareWindow не входит в число наследников Figure!

Как видно, диапазон этого вида полиморфизма в С++ ограничивается на этапе проектирования заданным перечнем типов. По умолчанию такой полиморфизм является статическим, но при использовании спецификатора virtual он превращается в динамический (см. позднее связывание):

class Figure
{
    ...
    virtual void Draw() const;
    ...
};
 
class Square : public Figure
{
    ...
    void Draw() const;
    ...
};
 
class Circle : public Figure
{
    ...
    void Draw() const;
    ...
};
 
Figure* figures[10];
figures[0] = new Square(1, 2, 10);
figures[1] = new Circle(3, 5, 8);
...
for (int i = 0; i < 10; i++)
    figures[i]->Draw();

В этом случае для каждого элемента массива будет вызвана Square::Draw() или Circle::Draw(), в зависимости от вида фигуры.

Чистой виртуальной функцией называется виртуальная функция-член, которая объявлена со спецификатором = 0 вместо тела:

class Figure
{
    ...
    virtual void Draw() const = 0;
);

Чистая виртуальная функция не имеет определения и не может быть непосредственно вызвана. Цель объявления такой функции — создать в общем базовом классе сигнатуру-прототип, которая не имела бы собственного определения, но позволяла создавать такие определения в классах-потомках и вызывать их через указатель на общий базовый класс. Функция-член объявляется чистой виртуальной тогда, когда её определение для базового класса не имеет смысла. Так, в вышеприведённом примере для базового класса Figure определение функции Draw() не имеет смысла, так как «фигур вообще» не бывает и их невозможно отобразить, но описание такой функции необходимо, чтобы можно было её переопределить в классах-потомках и вызывать методы Draw этих классов через указатель на Figure. Следовательно, вполне логично объявить Figure.Draw() как чистую виртуальную функцию-член.

С понятием чистой виртуальной функции в C++ тесно связано понятие «абстрактный класс». Абстрактным классом называется такой, у которого есть хотя бы одна не переопределённая чистая виртуальная функция-член. Экземпляры таких классов создавать запрещено, абстрактные классы могут использоваться только для порождения новых классов путём наследования. Если в классе-потомке абстрактного класса не переопределены все унаследованные чистые виртуальные функции, то такой класс также является абстрактным и на него распространяются все указанные ограничения.

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

Инкапсуляция[править | править исходный текст]

Основным способом организации информации в C++ являются классы. В отличие от структуры (struct) языка C, которая может состоять только из полей и вложенных типов, класс (class) C++ может состоять из полей, вложенных типов и функций-членов (member functions). Инкапсуляция в С++ реализуется через указание уровня доступа к членам класса: они бывают публичными (открытыми, public), защищёнными (protected) и собственными (закрытыми, приватными, private). В C++ структуры формально отличаются от классов лишь тем, что по умолчанию члены и базовые классы у структуры публичные, а у класса — собственные.

Доступ private protected public
Сам класс да да да
Друзья да да да
Наследники нет да да
Извне нет нет да

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

Пример класса, реализующего одномерный массив (это просто иллюстрация, а не образец дизайна!):

class Array
{
    public:
        Array() :
            len(0),
            val(NULL)
        {}
 
        Array(int _len) :
            len(_len)
        {
            val = new double[_len];
        }
 
        Array(const Array & a);
 
        ~Array()
        {
            Free();
        }
 
        inline const double & Elem(int i) const
        {
            return val[i];
        }
 
        inline void ChangeElem(int i, double x)
        {
            val[i] = x;
        }
 
    protected:
        void Alloc(int _len)
        {
            if (len == 0)
                Free();
 
            len = _len;
            val = new double[len];
        }
 
        void Free()
        {
            delete [] val;
            len = 0;
        }
 
        int len;
        double * val;
};

Здесь класс Array имеет 2 публичных функции-члена, 2 защищённых поля, 3 публичных конструктора и публичный деструктор. Описатель inline означает подсказку компилятору, что вместо вызова функции её код следует встроить в точку вызова, чем часто можно достичь большей эффективности.

Друзья[править | править исходный текст]

Функции-друзья — это функции, не являющиеся функциями-членами и тем не менее имеющие доступ к защищённым и закрытым членам класса. Они должны быть объявлены в теле класса как friend. Например:

class Matrix {
    ...
    friend Matrix Multiply(Matrix m1, Matrix m2);
    ...
};
 
Matrix Multiply(Matrix m1, Matrix m2) {
    ...
}

Здесь функция Multiply может обращаться к любым полям и функциям-членам класса Matrix.

Дружественной может быть объявлен как весь класс, так и функция-член класса. Если класс A — объявлен в классе B как друг, то все собственные (не унаследованные) функции-члены класса A могут обращаться к любым членам класса B. Например:

class Matrix {
    ...
    friend class Vector::getNum( Matrix & ) ;
    ...
private:
    int i;
};
...
class Vector
{
    int GetNum( Matrix & m ){ return m.i;} //обращение к закрытому члену данных класса Matrix
};

Пример обращения:

void SomeFunction()
{
    Matrix m;
    Vector v;
 
    int i = Vector::GetNum( m );
}

Четыре важных ограничения, накладываемых на отношения дружественности в C++:

  • Дружественность не транзитивна. Если A объявляет другом B, а B, в свою очередь, объявляет другом C, то C не становится автоматически другом для A. Для этого A должен явно объявить C своим другом.
  • Дружественность не взаимна. Если класс A объявляет другом класс B, то он не становится автоматически другом для A. Для этого должно существовать явное объявление дружественности A в классе B.
  • Дружественность не наследуется. Если A объявляет класс B своим другом, то потомки B не становятся автоматически друзьями A. Для этого каждый из них должен быть объявлен другом A в явной форме.
  • Дружественность не распространяется на потомков. Если класс A объявляет B другом, то B не становится автоматически другом для классов-потомков A. Каждый потомок, если это нужно, должен объявить B своим другом самостоятельно.

В общем виде это правило можно сформулировать следующим образом: «Отношение дружественности существует только между теми классами (классом и функцией), для которых оно явно объявлено в коде, и действует только в том направлении, в котором оно объявлено».

По действующему стандарту C++ вложенный класс не имеет прав доступа к закрытым членам объемлющего класса и не может быть объявлен его другом (последнее следует из определения термина друг как нечлена класса). В будущем стандарте C++0x эти ограничения будут устранены. В данном отношении современные версии компиляторов VC++, GNU C++ и Comeau C++ даже с отключенными расширениями следуют новым правилам, сформулированным в последних версиях черновика C++0x.

Конструкторы и деструкторы[править | править исходный текст]

В классах всегда есть специальные функции — конструкторы и деструкторы, которые могут быть объявлены явно или неявно.

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

Конструкторы обозначаются как одноимённые классу функции (например, Array::Array), деструкторы — как имя класса, предварённое тильдой (например, Array::~Array). Для конструкторов и деструкторов нельзя указывать тип возвращаемого значения. Деструктор нельзя объявлять как принимающий аргументы. Класс может иметь сколько угодно конструкторов (с разными наборами параметров), в том числе шаблонных, и только один (причём нешаблонный) деструктор.

Конструктор без параметров или конструктор, все параметры которого имеют аргументы по умолчанию, называется конструктором по умолчанию, нешаблонный конструктор с первым параметром-ссылкой на тот же класс (например, Array::Array(const Array&)) и остальными параметрами (если таковые есть), имеющими аргументы по умолчанию, — конструктором копирования, он вызывается при создании нового объекта, являющегося копией уже существующего объекта:

Array a(5); // вызывается Array::Array(int)
Array b; // вызывается Array::Array()
Array c(a); // вызывается Array::Array(const Array&)
Array d=a; // вызывается Array::Array(const Array&)
b=c; // происходит вызов оператора =
            // если он не определён (как в данном случае), то вызывается сгенерированный компилятором оператор присваивания, который
            // осуществляет копирование базовых подобъектов и почленное копирование нестатических членов-данных.
            // как правило конструктор копий и оператор присваивания переопределяются попарно

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

Конструкторы в C++ не могут быть объявлены виртуальными, а деструкторы — могут, и обычно так и объявляются, чтобы гарантировать правильное уничтожение доступного по ссылке или указателю объекта независимо от того, какого типа ссылка или указатель.

Перегрузка операторов[править | править исходный текст]

Функции-члены могут быть операторами:

class Array {
...
    inline double& operator[] (int n) { return val[n]; }

И далее

Array a(10);
...
double b = a[5];

Стандартная библиотека[править | править исходный текст]

Общая структура[править | править исходный текст]

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

Доступ к возможностям стандартной библиотеки C++ обеспечивается с помощью включения в программу (посредством директивы #include) соответствующих стандартных заголовочных файлов. Всего в стандарте C++11 определено 79 таких файлов. Средства стандартной библиотеки объявляются как входящие в пространство имён std. Заголовочные файлы, имена которых соответствуют шаблону «cX», где X — имя заголовочного файла стандартной библиотеки C без расширения (cstdlib, cstring, cstdio и пр.), содержат определения, соответствующие данной части стандартной библиотеки C, при этом программист может воспользоваться заголовочными файлами с именами по шаблону «cX.h» (добавлено стандартное расширение заголовочных файлов C «.h»), в которых те же средства определены как относящиеся к глобальному пространству имён.

Для использования следующих функций стандартной библиотеки

void* operator new(std::size_t) throw(std::bad_alloc);
void* operator new[](std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
void operator delete[](void*) throw();

подключение каких-либо заголовочных файлов не требуется.

Состав[править | править исходный текст]

Стандартная библиотека включает в себя следующие разделы:

  • Поддержка языка. Включает средства, которые необходимы для работы программ, а также сведения об особенностях реализации. Выделение памяти, RTTI, базовые исключения, пределы значений для числовых типов данных, базовые средства взаимодействия со средой, такие как системные часы, обработка сигналов UNIX, завершение программы.
  • Стандартные контейнеры. В стандартную библиотеку входят шаблоны для следующих контейнеров: одномерные массивы, списки, одно- и двунаправленные очереди, стеки, ассоциативные массивы, множества, очереди с приоритетом.
  • Основные утилиты. В этот раздел входит описание основных базовых элементов, применяемых в стандартной библиотеке, распределителей памяти и поддержка времени и даты в стиле C.
  • Итераторы. Обеспечивают шаблоны итераторов, с помощью которых в стандартной библиотеке реализуется стандартный механизм группового применения алгоритмов обработки данных к элементам контейнеров.
  • Алгоритмы. Шаблоны для описания операций обработки, которые с помощью механизмов стандартной библиотеки могут применяться к любой последовательности элементов, в том числе к элементам в контейнерах. Также в этот раздел входят описания функций bsearch() и qsort() из стандартной библиотеки C.
  • Строки. Шаблоны строк в стиле C++. Также в этот раздел попадает часть библиотек для работы со строками и символами в стиле C.
  • Ввод-вывод. Шаблоны и вспомогательные классы для потоков ввода-вывода общего вида, строкового ввода-вывода, манипуляторы (средства управления форматом потокового ввода-вывода в стиле C++).
  • Локализация. Определения, используемые для поддержки национальных особенностей и форматов представления (дат, валют и т. д.) в стиле C++ и в стиле C.
  • Диагностика. Определения ряда исключений и механизмов проверки утверждений во время выполнения (assert). Поддержка обработки ошибок в стиле C.
  • Числа. Определения для работы с комплексными числами, математическими векторами, поддержка общих математических функций, генератор случайных чисел.

Контейнеры, строки, алгоритмы, итераторы и основные утилиты, за исключением заимствований из библиотеки C, собирательно называются STL (Stanard Template Library — стандартная шаблонная библиотека). Изначально эта библиотека была отдельным продуктом и её аббревиатура расшифровывалась иначе, но потом она вошла в стандартную библиотеку C++ в качестве неотъемлемого элемента. В названии отражено то, что для реализации средств общего вида (контейнеров, строк, алгоритмов) использованы механизмы обобщённого программирования (шаблоны C++ — template). В работах Страуструпа подробно описываются причины, по которым был сделан именно такой выбор. Основными из них являются бо́льшая универсальность выбранного решения (шаблонные контейнеры, в отличие от объектных, могут легко использоваться для не объектных типов и не требуют наличия общего предка у типов элементов) и его техническая эффективность (как правило, операции шаблонного контейнера не требуют вызовов виртуальных функций и могут легко встраиваться (inline), что в итоге даёт столь же производительный код, как и при ручном кодировании).

Реализации[править | править исходный текст]

STL до включения в стандарт C++ была сторонней разработкой, в начале — фирмы HP, а затем SGI. Стандарт языка не называет её «STL», так как эта библиотека стала неотъемлемой частью языка, однако многие люди до сих пор используют это название, чтобы отличать её от остальной части стандартной библиотеки (потоки ввода-вывода (iostream), подраздел C и другие).

Проект под названием STLport[10], основанный на SGI STL, осуществляет постоянное обновление STL, IOstream и строковых классов. Некоторые другие проекты также занимаются разработкой частных применений стандартной библиотеки.

Отличия от языка C[править | править исходный текст]

Новые возможности[править | править исходный текст]

Нововведениями C++ в сравнении с C являются:

Новые возможности C++ включают объявления в виде выражений, преобразования типов в виде функций, операторы new и delete, тип bool, ссылки, расширенное понятие константности, подставляемые функции, аргументы по умолчанию, переопределения, пространства имён, классы (включая и все связанные с классами возможности, такие как наследование, функции-члены, виртуальные функции, абстрактные классы и конструкторы), переопределения операторов, шаблоны, оператор ::, обработку исключений, динамическую идентификацию и многое другое. Язык C++ также во многих случаях строже относится к проверке типов, чем C.

В C++ появились комментарии в виде двойной косой черты (//), которые были в предшественнике C — языке BCPL.

Некоторые особенности C++ позднее были перенесены в C, например, ключевые слова const и inline, объявления в циклах for и комментарии в стиле C++ (//). В более поздних реализациях C также были представлены возможности, которых нет в C++, например макросы va_arg и улучшенная работа с массивами-параметрами.

C++ не включает в себя C[править | править исходный текст]

Несмотря на то, что большая часть кода C будет справедлива и для C++, C++ не является надмножеством C и не включает его в себя. Существует и такой верный для C код, который неверен для C++. Это отличает его от Objective C, ещё одного усовершенствования C для ООП, как раз являющегося надмножеством C.

Существуют и другие различия. Например, C++ не разрешает вызывать функцию main() внутри программы, в то время как в C это действие правомерно. Кроме того, C++ более строг в некоторых вопросах; например, он не допускает неявное приведение типов между несвязанными типами указателей и не разрешает использовать функции, которые ещё не объявлены.

Более того, код, верный для обоих языков, может давать разные результаты в зависимости от того, компилятором какого языка он оттранслирован. Например, на большинстве платформ следующая программа печатает «С», если компилируется компилятором C, и «C++» — если компилятором C++. Так происходит из-за того, что символьные константы в C (например, 'a') имеют тип int, а в C++ — тип char, а размеры этих типов обычно различаются.

#include <stdio.h>
 
int main()
{
    printf("%s\n", (sizeof('a') == sizeof(char)) ? "C++" : "C");
    return 0;
}

Средства C, которых рекомендуется избегать[править | править исходный текст]

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

  • Не использовать макроопределения #define. Для объявления констант применять const, групп констант (перечислений) — enum, для прямого включения функций — inline, для определения семейств функций или типов — template.
  • Не использовать предварительные объявления переменных. Объявлять переменные в блоке, где они реально используются, всегда совмещая объявление с инициализацией.
  • Отказаться от использования malloc() в пользу оператора new, от realloc() — в пользу типа vector.
  • Избегать бестиповых указателей, арифметики указателей, неявных приведений типов, объединений, за исключением, возможно, низкоуровневого кода. Использовать «новые» преобразования типов, как более точно выражающие действительные намерения программиста и более безопасные.
  • Свести к минимуму использование массивов символов и строк в стиле C, заменив их на типы string и vector из STL. Вообще не стремиться создавать собственные реализации того, что уже имеется в стандартной библиотеке.

Дальнейшее развитие[править | править исходный текст]

Текущий стандарт языка ISO/IEC 14882:2003(E) был принят в 2003 году. Неофициально его обозначают как C++03. Следующая версия стандарта, опубликованная 1 сентября 2011 года, имеет неофициальное обозначение C++11.

Общие направления развития C++[править | править исходный текст]

По мнению автора языка Бьёрна Страуструпа[11][12][13], говоря о дальнейшем развитии и перспективах языка, можно выделить следующее:

  • В основном дальнейшее развитие языка будет идти по пути внесения дополнений в стандартную библиотеку. Одним из основных источников этих дополнений является известная библиотека boost.
  • Изменения в ядре языка не должны приводить к снижению уже достигнутой эффективности C++. С точки зрения Страуструпа, предпочтительнее внесение в ядро нескольких серьёзных больших изменений, чем множества мелких правок.
  • Базовыми направлениями развития C++ на ближайшее время является расширение возможностей и доработка средств обобщённого программирования, стандартизация механизмов параллельной обработки, а также доработка средств безопасного программирования, таких как различные проверки и безопасные преобразования типов, проверка условий и так далее.
  • В целом C++ спроектирован и развивается как мультипарадигменный язык, впитывающий в себя различные методы и технологии программирования, но реализующий их на платформе, обеспечивающей высокую техническую эффективность. Поэтому в будущем не исключено добавление в язык средств функционального программирования, автоматической сборки мусора и других отсутствующих в нём сейчас механизмов. Но в любом случае это будет делаться на имеющейся платформе высокоэффективного компилируемого языка.
  • Хотя формально одним из принципов C++ остаётся сохранение совместимости с языком C, фактически группы по стандартизации этих языков не взаимодействуют, а вносимые ими изменения не только не коррелируют, но и нередко принципиально противоречат друг другу идеологически. Так, элементы, которые новые стандарты C добавляют в ядро, в стандарте C++ являются элементами стандартной библиотеки и в ядре вообще отсутствуют, например, динамические массивы, массивы с фиксированными границами, средства параллельной обработки. Как считает Страуструп, объединение разработки этих двух языков принесло бы большую пользу, но оно вряд ли возможно по политическим соображениям. Так что практическая совместимость между C и C++ постепенно будет утрачиваться.

Стандарт C++11: дополнения в ядре языка[править | править исходный текст]

Явно определяемые константные функции и выражения constexpr.
В язык вводится ключевое слово constexpr, которым может быть помечено объявление функции, если она возвращает константу времени компиляции, или константной переменной, которая инициализируется выражением, содержащим только константы времени компиляции. Константные выражения могут использоваться везде, где по семантике программы требуется константа времени компиляции, например — в инициализаторах статических объектов. Предполагается, что константные выражения позволят программисту проще и точнее описывать семантику программы, а компилятору — шире применять предвычисление выражений на этапе трансляции кода.
Универсальная инициализация.
Инициализация массивов и объектов списками константных значений, заключёнными в фигурные скобки. Значительно упростит инициализацию сложных объектов.
Конструкторы и операторы присваивания с семантикой переноса.
Отдельное описание для конструкторов и присваиваний, реализующих перенос значения из источника в приёмник (то есть не гарантирующих сохранность объекта-источника после завершения операции). Во многих случаях такие конструкторы и присваивания могут быть описаны значительно более эффективно, чем обычные копирующие конструкторы и полноценные присваивания. Наличие их позволяет компилятору использовать эти более эффективные описания при работе с временными объектами (представляющими результаты выражений, аргументы или возвращаемые значения функций) что, в итоге, увеличивает производительность программ, работающих со сложными объектами.
Конструкторы и операторы преобразования типов — новые возможности.
Разрешён прямой вызов одного конструктора из другого, что уменьшает дублирование кода и исключает необходимость создания скрытых методов-инициализаторов. Для операторов преобразования типов разрешено описание с ключевым словом explicit — запрет на неявное преобразование типа параметра при вызове.
Вывод типов.
Для применения в шаблонах, там, где затруднительно указать конкретный тип переменной, введены два новых механизма: переменные типа auto и описание decltype. Оба используются на месте типа переменной при её объявлении. Переменная типа auto должна быть явно инициализирована некоторым выражением и её тип компилятор определяет по типу значения выражения-инициализатора. Соответственно, если в инициализации участвуют значения шаблонных типов или вызовы шаблонных функций, тип переменной определяется с учётом конкретных подставленных типов-параметров этих шаблонов. Описание вида «decltype(x) y» понимается как «y имеет тип выражения x», то есть компилятор выводит тип выражения x и определяет y как переменную этого типа. Все эти операции производятся только на этапе компиляции, то есть статическая типизация не нарушается.
Чтобы обеспечить возможность указывать через decltype тип возвращаемого значения функции, когда он зависит от типов параметров, введён альтернативный синтаксис объявления функции: auto func(params) -> type соответствует type func(params). Например, auto f(T x, T y) -> decltype(x+y) определяет функцию f как возвращающую тип, выводимый из выражения «x+y»[пояснения 1]
Цикл по коллекции.
Вслед за многими современными языками в C++ введена конструкция «цикл по коллекции» вида for(type &x : array){...}. Здесь тело цикла выполняется для каждого элемента коллекции array, а x в каждой итерации будет ссылаться на очередной элемент коллекции. В качестве коллекции может выступать C-массив или любой контейнер стандартной библиотеки, для которого определены итераторы begin и end.
Лямбда-выражения.
Добавлена возможность объявлять лямбда-выражения (безымянные функции, определяемые в точке применения), в том числе зависящие от внешних переменных (замыкания). Лямбда-выражения могут присваиваться переменным и использоваться везде, где требуется функция соответствующего типа, например, в алгоритмах стандартной библиотеки.
Изменения в описании виртуальных методов.
Добавлен необязательный модификатор override, который употребляется в объявлении метода, замещающего виртуальный метод родительского класса. Описание замещения с override вызывает проверку на наличие в родительском классе замещаемого метода и на совпадение сигнатур методов.
Добавлен также модификатор final, как и в Java, запрещающий дальнейшее замещение помеченного им метода. Также final может быть объявлен класс — в таком случае от него запрещено наследовать новые классы.
Изменения в механизме шаблонов.
Добавлена возможность описания шаблонов с неопределённым числом параметров и возможность объявления синонимов для неспециализированных или частично специализированных шаблонов. Ключевое слово export при объявлении специализации шаблона указывает компилятору на то, что фактически специализация сделана в другой единице компиляции, что позволяет избежать повторной специализации одних и тех же шаблонов одними и теми же параметрами.
Различные синтаксические дополнения.
Определено ключевое слово для константы — нулевого указателя: nullptr. Его использование позволяет избежать путаницы между целым нулевым значением и пустым указателем, которая возникает при использовании константы 0, как требовалось ранее.
Внесены изменения в семантику и, частично, синтаксис перечислений и объединений. Добавлена возможность создавать типобезопасные перечисления, с объединений снят ряд ограничений на структуру.
От компилятора требуется правильный лексический разбор текста программы с несколькими закрывающимися угловыми скобками подряд (ранее последовательность «>>» воспринималась однозначно как операция вывода в поток, поэтому в записи вложенных шаблонных конструкций требовалось обязательно разделять знаки «больше» пробелами или переводами строк).
sizeof теперь может возвращать размер члена класса по его имени, без указания конкретного экземпляра (sizeof(ClassA::b)).

Стандарт C++11: изменения в стандартной библиотеке[править | править исходный текст]

  • Добавлена библиотека <regex>, реализующая общепринятые механизмы поиска и подстановки с помощью регулярных выражений.
  • Добавлена поддержка многопоточности.

Проблемные моменты[править | править исходный текст]

Одним из камней преткновения стало ключевое слово export в шаблонах, где оно используется для разделения объявления и определения спецификации шаблона (нововведение стандарта «C++98»). Герб Саттер, секретарь комитета по стандартизации C++, рекомендовал убрать export из будущих версий стандарта по причине серьёзных сложностей в полноценной реализации, однако впоследствии его решили оставить.

Первым компилятором, поддерживающим export в шаблонах, стал Comeau C++ в начале 2003 года (спустя 5 лет после выхода стандарта C++98). В 2004 году бета-версия компилятора Borland C++ Builder X также начала его поддержку. Оба этих компилятора основаны на фронт-энде EDG. Другие компиляторы, такие как Microsoft Visual C++ или GCC (до версии 3.4.4), вообще этого не поддерживают. Поддерживают export: Microsoft Visual C++ 7.0, GCC 3.4.4, Microsoft Visual Studio 2010 и другие.

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

Примеры программ[править | править исходный текст]

Пример создания простой программы на языке C++ в Ubuntu GNU/Linux

Пример № 1[править | править исходный текст]

Это пример программы, которая ничего не делает. Она начинает выполняться и немедленно завершается. Она состоит из основного потока: функции main(), которая обозначает точку начала выполнения программы на C++.

int main()
{
    return 0;
}

Стандарт C++ требует, чтобы функция main() возвращала тип int. Программа, которая имеет другой тип возвращаемого значения функции main(), не соответствует стандарту C++.

Стандарт не говорит о том, что на самом деле означает возвращаемое значение функции main(). Традиционно оно интерпретируется как код возврата программы. Стандарт гарантирует, что возвращение 0 из функции main() показывает, что программа была завершена успешно.

Завершение программы на C++ с ошибкой традиционно обозначается путём возврата ненулевого значения.

Пример № 2[править | править исходный текст]

Эта программа также ничего не делает, но более лаконична.

int main(){}

В C++ (как и в C), если выполнение программы доходит до конца функции main(), то это эквивалентно return 0;. Это неверно для любой другой функции кроме main().

Пример № 3[править | править исходный текст]

Это пример программы Hello, world!, которая выводит сообщение, используя стандартную библиотеку, и завершается.

// это подключает заголовочный файл iostream
#include <iostream> 
 
using namespace std;
 
int main()
{
    cout << "Hello, world!" << endl; // можно и "...\n" вместо endl
    return 0;
}

Пример № 4[править | править исходный текст]

Современный C++ позволяет решать простым способом и более сложные задачи. Этот пример демонстрирует кроме всего прочего использование контейнеров стандартной библиотеки шаблонов (STL).

#include <iostream> // для использования std::cout
#include <vector> // для std::vector<>
#include <map> // для std::map<> и std::pair<>
#include <algorithm> // для std::for_each()
#include <string> // для std::string
 
using namespace std; // используем пространство имён "std"
 
void display_item_count(pair < string const, vector<string> > const& person)
{
   // person - это пара двух объектов: person.first - это его имя,
   // person.second - это список его предметов (вектор строк)
   cout << person.first << " is carrying " << person.second.size() << " items" << endl;
}
 
int main()
{
   // Объявляем карту со строковыми ключами и данными в виде векторов строк
   map< string, vector<string> > items;
 
   // Добавим в эту карту пару человек и дадим им несколько предметов
   items["Anya"].push_back("scarf");
   items["Dmitry"].push_back("tickets");
   items["Anya"].push_back("puppy");
 
   // Переберём все объекты в контейнере
   for_each(items.begin(), items.end(), display_item_count);
}

В этом примере для простоты используется директива использования пространства имён, в настоящей же программе обычно рекомендуется использовать объявления, которые аккуратнее директив:

#include <vector>
 
int main()
{
    using std::vector;
 
    vector<int> my_vector;
}

Здесь директива помещена в область функции, что уменьшает шансы столкновений имён (это и стало причиной введения в язык пространств имён). Использование объявлений, сливающих разные пространства имён в одно, разрушает саму концепцию пространства имён.[источник не указан 1514 дней]

Пример № 5[править | править исходный текст]

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

#include <iostream>
#include <numeric>
#include <boost/iterator/counting_iterator.hpp>
#include <boost/iterator/transform_iterator.hpp>
 
int odd(int i)
{
  return 2 * i + 1;
}
 
int square(int i)
{
  return i * i;
}
 
typedef boost::counting_iterator <int> counter;
typedef boost::transform_iterator <int (*)(int), counter> transformer;
 
transformer odds(int n)
{
  return transformer(counter(n), odd);
}
 
transformer squares(int n)
{
  return transformer(counter(n), square);
}
 
int main()
{
  using namespace std;
 
  cout << "Enter vector length: ";
  int n; cin >> n;
 
  cout << inner_product( odds(0), odds(n), squares(0), 0 ) << endl;
}

Данный пример демонстрирует так называемый «плоский» стиль записи. Это название связано с тем, что алгоритмы STL позволяют записывать код без циклов, соответственно ширина отступов в отформатированном коде примерно постоянна. Сторонники такого подхода считают, что программисту, знакомому со стандартной библиотекой С++, достаточно строчки с вызовом inner_product(), чтобы понять, что делает программа. С этой точки зрения вызов inner_product близок к словесному описанию задачи: «вычислить скалярное произведение векторов нечётных чисел и квадратов для значений от нуля до n».

Достоинства языка и его критика[править | править исходный текст]

Особенности[править | править исходный текст]

С одной стороны, С++ является потомком Симулы, которую Алан Кэй определил[14] как «Алгол с классами», и потому будет актуальной оценка С++ в сравнении с другими языками из семейства потомков Алгола (Basic, Pascal, Java, C#, Visual Basic, Delphi, D, Oberon и пр.). С другой стороны, С++ претендует на мультипарадигменность и универсальную применимость (в отличие от Си, ориентированного на очень узкий круг задач), и используется в промышленности намного шире других потомков Алгола, и потому будет актуальной оценка С++ в сравнении со всем многообразием применяемых языков, включая и Си. Во избежание повторений, оценки обычно совмещаются.

С++ — язык, складывающийся эволюционно. В отличие от языков с формальным определением семантики (см. Спецификация языков программирования), каждый элемент С++ заимствовался из других языков отдельно и независимо от остальных элементов (ничто из предложенного С++ за всю историю его развития не было новшеством в Computer Science), что сделало язык чрезвычайно сложным, со множеством дублирующихся и взаимно противоречивых элементов, блоки которых основаны на разных формальных базах. В этом отношении С++ повторяет путь PL/1, но, в отличие от последнего, длительное повсеместное использование С++ обеспечил выбор языка Си в качестве отправной точки.

Критики С++ не противопоставляют ему какой-либо конкретный язык, а наоборот, утверждают, что для всякого случая применения С++ всегда существует альтернативный инструментарий, позволяющий решить ту же задачу более эффективно и качественно. В свою очередь, сторонники С++ считают некорректным сравнивать различные аспекты С++ с совершенно различными языками, так как общий набор средств и возможностей С++ существенно шире, чем в большинстве языков, с которыми проводится сравнение, и сама по себе широта возможностей, на их взгляд, является веским оправданием несовершенства каждой отдельно взятой возможности. Более того, по их мнению, высокая совместимость с Си является одной из принципиальных черт языка, и потому все недостатки С++ оправданы преимуществами, предоставляемыми этой совместимостью (см. раздел Философия C++). При этом сторонники C++ игнорируют подтверждённый исследованиями[15][16] факт, что декомпозиция проекта на несколько разных языков, наиболее пригодных для своих мини-задач, (или просто использование одного наилучшим образом подходящего языка) сокращает на порядок общую трудоёмкость разработки при одновременном повышении на порядки основных показателей качества программирования. По этой причине критики не соглашаются рассматривать недостатки С++ по отдельности и тем более делать поправку на «универсальность», утверждая, что если задача требует одновременно высокоуровневых и низкоуровневых возможностей, то использование Си совместно с языками, из которых заимствованы отсутствующие в Си возможности C++, будет более разумным, чем внедрение этих возможностей в сам Си и использование полученного языка резко возросшей степени внутренней сложности.

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

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

С++ заявляется как кроссплатформенный: стандарт языка накладывает минимальные требования на ЭВМ для запуска скомпилированных программ. На практике, для написания портируемого кода на С++ требуется огромное мастерство и опыт, и «небрежные» коды на С++ с высокой вероятностью могут оказаться непортируемыми[17]. Тонкое владение С++ в принципе может сделать код на С++ столь же портируемым, что и код на Си (хотя, по мнению Линуса Торвальдса, С++ при этом фактически сократится до своего подмножества Си[18]). Однако, критики С++ утверждают, что изучение и использование одновременно всех языков, противопоставляемых С++ (не вызывающих серьёзных проблем при портировании), в сумме требует примерно тех же интеллекта, усилий и временных затрат, что и изучение и использование одного только С++ на высококлассном уровне — в связи с чем становится актуальной также оценка порога вхождения и результативности (производительности и качества труда программистов).

Многие аспекты в спорах «за и против С++» обусловлены расхождением в сущностном понимании процесса стандартизации. Б.Страуструп и его последователи считают, что «стандарт — это контракт между программистами, разрабатывающими программы на языке, и программистами, разрабатывающими компиляторы языка»[19]. Каждый новый стандарт С++ являлся декларацией того, что отныне должно быть реализовано во всех компиляторах — при том, что С++ имеет естественное определение семантики, то есть потенциально зависим от реализации (стандарт содержит множество пунктов, определённых как «implementation-defined»). Традиционно, успешная стандартизация в технике представляет собой формальный перевод стандарта из статуса де-факто в статус де-юре (подытоживание общепринятых, устоявшихся знаний для обеспечения надёжной внутриотраслевой совместимости). Все противопоставляемые С++ языки программирования, если проходили процедуру стандартизации (что не обязательно для портируемости), то лишь после многолетней апробации на практике; при этом все они изначально имеют формальное определение семантики, так что накапливающиеся к моменту стандартизации изменения от первой версии языка оказываются не принципиальны. Теоретически эти определения стандартизации тождественны. На практике определение Страуструпа согласуется с традиционным лишь при условии, что нет абсолютно никаких препятствий против немедленного и беспрекословного подчинения статуса де-факто декларированному де-юре — то есть если абсолютно все компиляторы реализуют поддержку нового стандарта сразу после его выхода и со 100 % соответствием ему, и что новый стандарт не отменяет никаких положений старого, кроме признанных убыточными или вредоносными. Так может происходить лишь в условиях полного отсутствия человеческого фактора (иначе говоря, при отсутствии программистов как таковых). На практике именно этим и обусловлено значительное отставание реальной кроссплатформенности С++ от заявленной разработчиками стандарта и противоречивость предлагаемых возможностей. Сторонники С++ считают взгляд Страуструпа на стандартизацию более практико-ориентированным и принимают порождаемые им проблемы за должное.

Достоинства[править | править исходный текст]

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

  • Высокая совместимость с языком Си : код на Си может быть с минимальными переделками скомпилирован компилятором C++. Внешнеязыковой интерфейс является прозрачным, так что библиотеки на Си могут вызываться из C++ без дополнительных затрат, и более того — при определённых ограничениях код на С++ может экспортироваться внешне не отличимо от кода на Си (конструкция extern "C").
  • Как следствие предыдущего пункта — вычислительная производительность. Язык спроектирован так, чтобы дать программисту максимальный контроль над всеми аспектами структуры и порядка исполнения программы. Один из базовых принципов С++ — «не платишь за то, что не используешь» (см. Философия C++) — то есть ни одна из языковых возможностей, приводящая к дополнительным накладным расходам, не является обязательной для использования. Имеется возможность работы с памятью на низком уровне.
  • Автоматический вызов деструкторов объектов в адекватном порядке (обратном вызову конструкторов) упрощает и повышает надёжность управления памятью и другими ресурсами (открытыми файлами, сетевыми соединениями, соединениями с базами данных и т.п.).
  • Перегрузка операторов позволяет кратко и ёмко записывать выражения над пользовательскими типами в естественной алгебраической форме.
  • Имеется возможность управления константностью объектов (модификаторы const, mutable, volatile). Использование константных объектов повышает надёжность и служит подсказкой для оптимизации. Перегрузка функций-членов по признаку константности позволяет определять выбор метода в зависимости цели вызова (константный для чтения, неконстантный для изменения). Объявление mutable позволяет сохранять логическую константность при виде извне кода, использующего кэши и ленивые вычисления.
  • Возможность расширения языка для поддержки парадигм, которые не поддерживаются компиляторами напрямую. Например, библиотека Boost.Bind позволяет связывать аргументы функций. Используя шаблоны и множественное наследование, можно имитировать классы-примеси и комбинаторную параметризацию библиотек. Такой подход применён в библиотеке Loki, класс SmartPtr которой позволяет, управляя всего несколькими параметрами времени компиляции, сгенерировать около 300 видов «умных указателей» для управления ресурсами.
  • Доступность. Для С++ существует огромное количество учебной литературы, переведённой на всевозможные языки. Язык имеет низкий порог вхождения, но среди всех языков такого рода обладает наиболее широкими возможностями.

Критика[править | править исходный текст]

Сторонники С++ позиционируют его как «универсально применимый» — вплоть до отождествления «применимости» с Тьюринг-полнотой (что является ошибкой) и одновременно с оптимальностью, то есть обоснованностью выбора его в качестве инструмента для данной конкретной задачи; при этом ни одной конкретной задачи не обозначается, а наоборот, делается утверждение, что С++ подходит для любой задачи (что теоретически невозможно[пояснения 2])[источник не указан 180 дней]. Однако, С++ не отвечает многим требованиям качества программирования, не предъявляемым к Си, но важным для широкого спектра задач прикладного программирования[источник не указан 180 дней]. В частности, критики[источник не указан 180 дней] полагают, что:

  • Плохо продуманный синтаксис сужает спектр применимости языка (что, с учётом претензий на "универсальность", делает его крайне неудобным в некоторых задачах)[источник не указан 180 дней].
  • Унаследованные от Си низкоуровневые свойства существенно тормозят и затрудняют прикладную разработку[источник не указан 180 дней].
  • Язык не содержит многих важных возможностей.
  • Язык содержит опасные возможности, существенно снижающие качество программ сразу по всем показателям[источник не указан 180 дней].
  • Языку присущи проблемы вычислительной производительности[источник не указан 180 дней].
  • Производительность труда программистов на языке оказывается неоправданно низка, а продукт труда — низкокачественным.

В то же время, критике подвергается и применимость С++ в низкоуровневой разработке в качестве «улучшенного Си»[источник не указан 180 дней].

Многие конкретные недостатки вытекают непосредственно из свойств семантики системы типов языка: она не отвечает требованиям полноты и ортогональности, при этом обладает избыточностью и предусматривает понятие «приведения типов» (как явно, так и неявно)[источник не указан 180 дней]. В отношении типизации, С++ чаще всего противопоставляются либо типизируемые по Хиндли-Милнеру, либо динамически типизируемые языки[источник не указан 180 дней]. Начиная со стандарта C++0x, в языке появилась возможность автоматического выведения типов, из-за чего возникло заблуждение, что отныне «С++ поддерживает вывод типов по Хиндли-Милнеру». Однако, системе типов С++ противопоставляется не сам механизм выведения типов, а полиморфная семантика системы типов Хиндли-Милнера, предусматривающая в том числе и механизм выведения, но не как главное преимущество[источник не указан 180 дней]. Существуют примеры развития Си по пути типизации Хиндли-Милнера (см. раздел Влияние и альтернативы)[источник не указан 180 дней].

Критика C++ с позиций только ООП (без сравнения методологий проектирования) с описанием вреда от влияния C++ на другие языки приведена в работе[20]. В случае языково-ориентированного проектирования программ применимость С++, как и при использовании любых других языков, ограничивается нижним уровнем системы — реализацией предметно-специфичных языков (DSL) первого уровня. Для этой задачи С++ объективно является далеко не оптимальным выбором (см. раздел Отсутствие возможностей). В случае применения методологии «чистого встраивания» DSL в язык общего назначения[16] (которая является традиционной для Lisp/ML, и для которой в С++ потенциально предназначена библиотека Boost.Spirit), с т.з. воплощения изоморфизма Карри-Ховарда выбор С++ в качестве базы был бы абсурден (см. ниже)[источник не указан 180 дней].

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

Синтаксис[править | править исходный текст]

Громоздкость 
Определение синтаксиса является одним из самых громоздких и продолжает расширяться. Стандарт 2003 г. содержал уже более 200 строк РБНФ, усложнённых наличием среди них массы неоднозначностей (англ. disambiguations). Даже семантически более мощные языки имеют существенно более простой синтаксис (например, РБНФ языка Scheme по стандарту R6RS составляет всего 12 основных строк плюс 11 строк т. н. «синтаксического сахара», т. е. необязательных). Громоздкость синтаксиса С++ порождает массу проблем:[источник не указан 180 дней].
  • становится невозможным редактирование синтаксиса языка программистом для адаптации к требованиям предметной области (как это возможно, например, в OCaml посредством управления модулем camlpX компилятора, или в метаязыках посредством синтаксических макросов)[источник не указан 180 дней].
  • практически гарантированы (предсказуемы с очень высокой вероятностью в силу человеческого фактора) отклонения от стандарта в различных компиляторах, что обеспечивает низкую портируемость программ на C++ между компиляторами (см. раздел Особенности). Большинство промышленных языков (даже не стандартизированных) защищено от этого формальной семантикой[источник не указан 180 дней].
Принуждение к избыточности 
Многие языки предлагают избыточность — несколько перегружающих друг друга способов получить один и тот же результат, так что использующие их программисты оказываются свободны в выборе; при этом в иных ситуациях эти языковые элементы оказываются полезны каждый по своему[источник не указан 180 дней]. С++ вынуждает использовать необоснованно перегружающие друг друга элементы, что приводит к неделимому перемешиванию «что» и «как» в программировании (функциональности и её реализации)[20]. Доступ к данным («что»-операция) в С++ осуществляется двумя «как»-операциями: «.» и «->» в зависимости от того, как именно в данном контексте реализовано представление данных. Полиморфизм так же имеет две реализации: простую и виртуальную. Наряду с наследованным из Си управлением памятью через malloc/free добавляется управление памятью через new/delete и new[]/delete[], и не осуществляется никакого контроля за парностью[источник не указан 180 дней]. Можно выделить блок с помощью new[], а затем высвободить с помощью delete или даже free — это приводит к скрытым ошибкам и нестабильной работе программ (т.к. new и new[] реализованы через malloc, но выполняют больше инициализаций, подчистка которых в этом случае не производится)[источник не указан 180 дней]. То же касается приведения типов — наряду с наследованным из Си простым приведением типов, в С++ добавлены специальные операции — dynamic_cast, static_cast, const_cast, reinterpret_cast[источник не указан 180 дней].

Тяжелое наследие[править | править исходный текст]

С++ унаследовал от Си множество низкоуровневых свойств, из-за которых использование высокоуровневых конструкций затрудняется даже в задачах, где низкоуровневые возможности сами по себе не используются[источник не указан 180 дней]. В книге Страуструпа "Дизайн и эволюция C++" [19] это оправдывается попыткой сохранить обратную совместимость с Си, где эти свойства изначально оправданы и не являются недостатками. Однако, при рассмотрении С++ как самостоятельного языка прикладного программирования, эти свойства превращаются в недостатки[источник не указан 180 дней], т.к. приводят к неоправданному существенному затруднению процесса прикладной разработки и невозможности включить в процесс не владеющих программированием специалистов предметной области задачи, как это описано в [16][источник не указан 180 дней].

Сложное поведение между точками следования 
Операции присваивания (=), инкремента (++), декремента (--) и др. являются в Си выражениями, возвращающими значение, а не формируют самостоятельную императивную команду, как в других императивных языках — и С++ унаследовал это свойство[источник не указан 180 дней]. Однако, в отличие от простых выражений, данные операции осуществляют изменение состояния[источник не указан 180 дней]. В нечистых ФЯ (Lisp, ML) изменение состояния обычно делается самостоятельной императивной командой, не возвращающей значения, что делает их как минимум хорошо заметными при чтении кода. В С++ же это позволяет создавать трудночитаемые выражения, размещая сложное поведение между точками следования, что влечёт труднообнаружимые ошибки и снижает портируемость[источник не указан 180 дней]. В Си такая семантика, несмотря на её опасность, была предназначена для обеспечения возможности ручной оптимизации кода (немедленное использование значения выражения позволяет избавиться от ненужных ассемблерных команд пересылки данных)[источник не указан 180 дней]. Однако, в настоящее время оптимизирующие компиляторы обычно генерируют оптимальный код и на традиционных выражениях (устраняя ненужные ассемблерные команды автоматически)[21], так что выгода от этой возможности С++ более не компенсирует порождаемые ею проблемы в прикладном программировании.
Разрушающее присваивание вместо ветвления 
Операции разрушающего присваивания (=) и логического сравнения (==) синтаксически схожи. Поскольку присваивание является в С++ операцией, возвращающей значение, то замена сравнения на присваивание в любом контексте является синтаксически и семантически корректным, но в корне изменяет поведение программы, что влечёт труднообнаружимые ошибки. Типичный пример подобной ошибки:
if (x=0) { операторы }
Здесь в условном операторе записан один символ равенства вместо двух, т.е. операция присваивания вместо операции сравнения. В результате, вместо того, чтобы сравнить текущее значение x с нулём, программа присвоит x нулевое значение, затем возьмёт результат этого выражения (ноль), выполнит неявное приведение типа к булеву значению «ложь» (поскольку так того требует семантика оператора if), и блок операторов в условной конструкции не выполнится никогда. В развитых компиляторах С++ предлагается диагностика некоторых подобных синтаксических случаев с выдачей предупреждений, но реализация подобной диагностики для С++ весьма трудоёмка.
Приведение типов 
Некоторые преобразования типов не интуитивны, хотя могут производиться неявно. В частности, операция над беззнаковым и знаковым числами порождает беззнаковый результат; однако, в аксиоматике теории множеств множество натуральных чисел (N) является подмножеством множества целых (Z), а совместное использование элементов разных множеств допустимо лишь на уровне общего надмножества — т.е. интуитивным было бы получение в результате целого (знакового) числа, но в С++ происходит наоборот, и возникает риск труднообнаружимой ошибки[17].
Операторы управления 
C++, как и Си, позволяет пропускать break в ветви оператора switch с целью последовательного выполнения нескольких ветвей, что влечёт труднообнаружимые ошибки. Во многих языках семантика оператора выбора более строгая. В C# необходимо всегда писать либо break, либо использовать goto case N для явного указания порядка выполнения[22]. В потомках ML при сопоставлении с образцом не только синтаксически строго обозначаются границы блоков ветвлений, но и контролируется совпадение количества вариантов с определением разбираемого вариантного типа. По аналогии с ML, допущенный в Си недостаток его создатели (Керниган и Ритчи) исправили в языке Limbo.

Отсутствие возможностей[править | править исходный текст]

Рефлексивное метапрограммирование 
Рефлексивное метапрограммирование в С++ невозможно (равно как и во всех остальных потомках Алгола). Интроспекция предусмотрена, но реализована отдельно от основной системы типов, что делает её практически бесполезной. Наибольшее, что можно получить — параметризацию поведения на заранее известном диапазоне случаев. Это является препятствием против применения С++ в большинстве подходов к реализации Искусственного Интеллекта (за исключением самых примитивных, реализующих статический алгоритм).
Порождающее метапрограммирование 
Порождающее метапрограммирование на основе шаблонов C++ трудоёмко и ограничено по возможностям. Оно осуществляется за счёт реализации статического (исполняемого на этапе компиляции) интерпретатора примитивного функционального языка программирования посредством шаблонов C++, а также примитивного препроцессора, унаследованного от Си. Сама по себе данная возможность весьма привлекательна, но, во-первых, код на шаблонах С++ имеет крайне низкие показатели понимаемости и тестируемости; во-вторых, при разворачивании шаблонов порождается неэффективный код, т.к. язык шаблонов не предоставляет никаких средств для оптимизации (см. также раздел Вычислительная производительность). При этом встраиваемые предметно-специфичные языки, реализуемые таким образом, всё равно требуют знания самого С++ для их использования, т.к. возводимые барьеры абстракции сохраняют все свойства нижележащей реализации, что не обеспечивает полноценное разделение труда. Таким образом, возможности С++ по расширению возможностей самого С++ весьма ограничены. Перечисленных недостатков лишены метаязыки, такие как диалекты Lisp и ML, их потомки и гибриды (Haskell, Nemerle), а также Рефал, и потому они являются оптимальным выбором для задачи разработки языков тем или иным способом. В Common Lisp, Nemerle и Template Haskell имеются подсистемы метапрограммирования, делающие доступным свободное определение синтаксиса операций; с их помощью становится возможным встраивание в код предметно-ориентированных языков, не требующих знания основного языка, что обеспечивает эффективное разделение труда разработчиков[23][24]; подобные возможности иным способом предоставляют SML и OCaml. Причиной отставания мощности языка шаблонов С++ является то, что, в отличие от метаязыков, где в качестве подсистемы метапрограммирования используется сам основной язык, в С++ язык шаблонов представляет собой отдельный язык, не совпадающий и не пересекающийся с самим С++ (который к тому же не был задуман создателями языка, а был спонтанно обнаружен хакерами), из-за чего потенциал роста сложности абстракций оказывается ограниченным. В языке D также реализована подсистема шаблонного метапрограммирования, сравнимая по мощности с оной в С++, но более простая в применении.
Функциональное программирование 
Явная поддержка функционального программирования присутствует только в стандарте C++0x, вышедшим лишь после почти 30 лет развития языка. До этого данный пробел устранялся различными библиотеками (Loki, Boost), использующими язык шаблонов для расширения основного языка функциональными конструкциями. Качество подобных решений значительно уступает качеству встроенных в функциональные языки решений[пояснения 3] и качеству реализаций высокоуровневых возможностей С++ (таких как ООП) посредством функциональных языков. Все реализованные в C++ возможности ФП оказываются лишь их эмуляцией и используются совместно с императивными возможностями, что не даёт возможности применения присущих ФП мощных оптимизационных методик (см. раздел Вычислительная производительность). Кроме того, из-за трудоёмкости использования шаблонов, на практике ФП в С++ обычно ограничивается вызовами функциональных библиотек и реализацией отдельных методов, и практически не даёт преимуществ в проектировании программ (см. Соответствие Карри — Ховарда и пред.пункт), так что оно в С++ по-прежнему осуществляется обычно лишь посредством объектно-ориентированной декомпозиции.
Контроль за поведением 
Принцип С++ «не платишь за то, что не используешь» (см. Философия C++) заявляется как средство обеспечения высокой скорости исполнения. На практике он лишь приводит к чрезмерному использованию ситуативного (ad hoc) полиморфизма — явного описания различного поведения даже для редких ситуаций под единым идентификатором — т.е. перегрузки функций. В С++ предусмотрено сразу три формы перегрузки, что приводит к значительному дублированию кода. Перегрузка операторов призвана дать возможность введения в программу т.н. "синтаксического сахара", но в С++ может поощрять к бесконтрольному изменению поведения элементарных операций, в т.ч. new/delete и new[]/delete[], для разных типов (что резко повышает риск разного рода ошибок). Это обусловлено, во-первых, тем, что вводить новый синтаксис нельзя (хотя синтаксис стандартных операторов С++ адекватен семантике далеко не всех типов, которые может потребоваться ввести в программу); а во-вторых, тем, что всякий учебник даже для низкого порога вхождения показывает, как перегружать те или иные операторы, включая и управление временем жизни объектов. (Обычно ввод специального синтаксиса для операций является совершенно безболезненным в аппликативных языках, где эта возможность существует независимо от полиморфной семантики системы типов и не предоставляет доступа к управлению памятью.) Некоторые интуитивно ожидаемые операции (подчистка динамических объектов в случае генерации исключений) в С++ не выполняются в соответствии с означенным принципом; в то же время, значительная часть перегруженных функций и операторов вызывается неявно (приведение типов, создание временных экземпляров классов и др.). Попутно идеология языка спутывает «контроль за поведением» с «контролем за эффективностью» — что представляет опасность, т.к. де-факто возможности явного контроля этих аспектов исполнения программы со стороны человека являются взаимоисключающими[пояснения 4]. В сочетании с изобилием побочных эффектов всё это приводит к тому, что по мере роста сложности системы код на С++ не абстрагируется, а, наоборот, усложняется, и значительно снижаются показатели понимаемости и тестируемости — возникает необходимость контролировать (как чтением, так и отладкой) слои реализации по разные стороны от текущего барьера абстракции, что считается плохой практикой в программировании. В результате трудоёмкость (а значит, и стоимость) разработки растёт от объёма реализованной функциональности по вогнутому закону (в языках с полиморфной семантикой системы типов этот рост характеризуется выпуклой кривой за счёт существенно более высокого показателя повторного использования кода).
Модульность и абстракция 
Как отмечает Ян Джойнер[20], С++ ошибочно отождествляет инкапсуляцию и сокрытие (см. также раздел Идеология компонентности), при этом контролируя совпадение типов на уровне их идентификаторов, а не сигнатуры. Как следствие, оказывается невозможно подменять модули (классы), основываясь на совпадении их интерфейсной функциональности. Т.е. если некоторые возможности отсутствуют в С++, но реализованы на уровне библиотек (такие как сборка мусора или длинная арифметика), то для получения выгоды от них необходимо вручную модифицировать уже имеющийся код для адаптации его под новый модуль. В языках самых разных семантик (ML, Smalltalk, Erlang, Python, и даже Си) полиморфизм, инкапсуляция и сокрытие являются независимыми (ортогональными) категориями, а контроль типов осуществляется по соответствию их сигнатур, а не идентификаторов,— так что выгода от нового кода обеспечивается простой подменой имеющегося модуля на новый, что и называется абстракцией. В С++, как отмечает Линус Торвальдс[25], код кажется абстрактным лишь до тех пор, пока не возникает необходимость его изменить.

Избыточные и опасные возможности[править | править исходный текст]

Гарантия некорректности 
Из-за слабой системы типов программист оказывается волен легко нарушить заданную в конкретном случае дисциплину программирования. Например, хотя модификатор const предназначен для повышения предсказуемости поведения (что должно облегчить доказательство корректности, и, как следствие, расширить возможности оптимизации), но модификатор mutable предназначен именно для принудительного разрешения изменения состояния внутри константного объекта. Это значит, что код сторонней библиотеки на С++ может содержать изменяемое состояние вопреки любым попыткам назначить ему извне свойство константности. Более того, допускается динамически удалить атрибут const с константного объекта, превращая его в леводопустимый (L-value). Сама по себе явная декларация в спецификации языка подобных возможностей делает попытки формальной верификации бессмысленными.
Неконтролируемая макроподстановка 
Средства макроподстановки Си (#define) являются сколь мощным, столь же опасным средством. Они сохранены в C++ несмотря на то, что для решения всех[источник не указан 232 дня] задач, для которых они были предусмотрены в Си, в С++ были предоставлены более строгие и специализированные средства — шаблоны, перегрузка функций, inline-функции, пространства имён, более развитая типизация, расширение применения модификатора const, и др. В унаследованных от Си стандартных библиотеках много потенциально опасных макросов[26]. Шаблонное метапрограммирование также порой совмещается с использованием макроподстановки для обеспечения т. н. «синтаксического сахара».
Неэкономная экономия 
В контексте задач, для решения которых разработан Си, считается опасным расширение его до С++, и не только из-за искажения имён (name mangling), увеличения библиотеки времени исполнения (RTL) и раздувания кода использованием шаблонов (см. раздел Вычислительная производительность), но в большей степени из-за присущей С++ идеологии программирования. Из того, что Си является подмножеством С++, вообще говоря, не следует, что С++ должен быть однозначно лучше, чем Си; и противники С++ утверждают, что в данном случае расширение возможностей системы привело к серьёзному ухудшению её свойств. Например, Линус Торвальдс придерживается такого мнения:
С++ — кошмарный язык. Его делает ещё более кошмарным тот факт, что множество недостаточно грамотных программистов используют его, до такой степени, что оказывается намного проще выкинуть его как мусор. Откровенно говоря, даже если нет *никаких* причин для выбора Си, кроме того чтобы держать С++-программистов подальше — то одно это уже будет достаточно веским основанием для использования Си.
...Я пришёл к выводу, что *действительно* предпочту выгнать любого, кто предпочтёт вести разработку проекта на С++, нежели на Си, чтобы этот человек не загубил проект, в который я вовлечён.
С++ приводит к очень, очень плохим проектным решениям. Неизбежно начинают применяться "замечательные" библиотечные возможности вроде STL, и Boost, и прочего мусора, которые могут "помочь" программированию, но порождают:
— невыносимую боль, когда они не работают (и всякий, кто утверждает, что STL и особенно Boost стабильны и портируемы, настолько погряз во лжи, что это даже не смешно)
— неэффективно абстрагированные программные модели, когда спустя два года обнаруживается, что какая-то абстракция была недостаточно эффективна, но теперь весь код зависит ото всех окружающих её замечательных объектных моделей, и её нельзя исправить, не переписав всё приложение.
Другими словами, единственный способ иметь хороший, эффективный, низкоуровневый и портируемый С++ сводится к тому, чтобы ограничиться всеми теми вещами, которые элементарно доступны в Си. А ограничение проекта рамками Си будет означать, что люди его не выкинут, и что будет доступно множество программистов, действительно хорошо понимающих низкоуровневые особенности и не отказывающихся от них из-за идиотской ерунды про "объектные модели".
... когда эффективность является первостепенным требованием, "преимущества" С++ будут огромной ошибкой.

Линус Торвальдс, [27]

Более эффективными и качественными инструментами в этой сфере могут быть другие потомки Си (см. раздел Влияние и альтернативы).

Вычислительная производительность[править | править исходный текст]

Результирующий объём исполнимого кода 
Использование шаблонов C++ представляет собой параметрический полиморфизм на уровне исходного кода, но при трансляции он превращается в ситуативный (ad hoc) полиморфизм (то есть перегрузку функций), что приводит к существенному увеличению объёма машинного кода в сравнении с языками, имеющими истинно полиморфную систему типов (потомками ML). Для снижения размера машинного кода пытаются автоматически обрабатывать исходный код до этапа раскрутки шаблонов[28][29]. Другим решением могла бы быть стандартизованная ещё в 1998 году возможность экспорта шаблонов, но она доступна далеко не во всех компиляторах, так как её трудно реализовать[30][31][мнения 3] и для импорта библиотек шаблонов С++ в языки с существенно отличной от С++ семантикой она всё равно была бы бесполезна. Сторонники С++ оспаривают масштабы раздувания кода как преувеличенные[32], игнорируя даже тот факт, что в Си параметрический полиморфизм транслируется непосредственно, то есть без дублирования тел функций вообще. При этом сторонники С++ считают, что параметрический полиморфизм в Си опасен — то есть более опасен, чем переход от Си к С++ (противники С++ утверждают обратное — см. выше).
Потенциал к оптимизации 
Из-за слабой системы типов и изобилия побочных эффектов становится крайне затруднительным эквивалентное преобразование программ, а значит и встраивание в компилятор многих важных оптимизирующих алгоритмов — автоматического распараллеливания, устранения ненужных промежуточных представлений данных и вычислений, лямбда-лифтинга, вызовов процедур с передачей продолжений, суперкомпиляции и др. В результате реальная эффективность программ на С++ ограничивается имеющейся квалификацией программистов и вложенными в конкретный проект усилиями, и «небрежная» реализация может существенно уступать по эффективности «небрежным» реализациям на языках более высокого уровня, что подтверждается сравнительными испытаниями языков[33]. Это является существенным препятствием против применения С++ в индустрии data mining.
Эффективное управление памятью 
Потенциал к повышению эффективности управления памятью весьма ограничен. Хотя существуют многочисленные реализации автоматической сборки мусора, использование более эффективных способов управления памятью (таких как статический вывод регионов) для конкретной библиотеки невозможно (точнее, это привело бы к реализации поверх языка С++ интерпретатора нового языка, сильно отличающегося от С++ как большинством объективных свойств, так и общей идеологией) по причине необходимости прямого доступа к AST. Для автоматического управления памятью в С++ традиционно используются т. н. «умные указатели», ручное же управление памятью снижает эффективность самих программистов (см. раздел Результативность).
Замедление Си 
В условиях узко ограниченных вычислительных ресурсов (например, при программировании многих встраиваемых систем) неприемлемыми могут оказаться самые разные аспекты С++, отличающие его от Си. Механизм виртуальных функций реализуется посредством позднего связывания, то есть требует динамического вычисления реального адреса функции (RVA). Наконец, встраиваемая система может просто не располагать тем объёмом памяти, который необходим для библиотеки времени исполнения (RTL), сопровождающей любую программу на С++. Хотя формально стандарт не накладывает ограничения на состав этой библиотеки для конкретной программы на С++, при полном её удалении уже нельзя говорить об использовании языка С++ в качестве инструмента, так как большинство конструкций языка, связанные с RTL, окажутся утраченными (например, операции new/delete и new[]/delete[]) — будет иметь место язык Си или его подмножество, соответственно будет отличаться и идеология программирования.

Результативность[править | править исходный текст]

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

Ручное управление памятью 
Как отмечается в исследовании[34], программисты на Си тратят 30% — 40% общего времени разработки (не считая отладки) только на управление памятью. Если в низкоуровневых задачах (для которых разработан Си) это оправданно, то в прикладных задачах широкого спектра (на которые претендует С++) это не только является напрасным, но и чревато ошибками. Для С++ существуют библиотечные средства автоматического управления памятью, но они применяются далеко не всегда, кроме того, их эффективность ограничена (см. также разделы Отсутствие возможностей и Вычислительная производительность).
В то же время существует эмпирическое исследование[35], показывающее, что разница в скорости разработки программного обеспечения на C++ и на Java, в которой реализована автоматическая сборка мусора, невелика.
Менеджмент проектов 
Перечисленные выше факторы делают сложность менеджмента проектов на С++ одной из самых высоких в индустрии разработки ПО.
Джеймс Коггинс, в течение четырех лет ведущий колонку в The C++ Report, дает такое объяснение:
 — Проблема в том, что программисты, работающие в ООП, экспериментировали с кровосмесительными приложениями и были нацелены на низкий уровень абстракции. Например, они строили такие классы как «связанный список», вместо «интерфейс пользователя», или «луч радиации», или «модель из конечных элементов». К несчастью, строгая проверка типов, которая помогает программистам C++ избегать ошибок, одновременно затрудняет построение больших объектов из маленьких.

Ф. Брукс, Мифический человеко-месяц

Идеология компонентности 
Объектная модель С++, унаследованная от Симулы и дополненная двумя формами множественного наследования (простой и виртуальной), имеет не только объективные проблемы, но и опасную идеологию. По словам создателя Smalltalk Алана Кэя, объектная модель «Алгол с классами» обладает худшими характеристиками качества, чем модель «всё — объект»[14], и в этом смысле С++ уступает своему ближайшему конкуренту Objective-C: показатель повторного использования кода оказывается крайне низким (см. раздел Полиморфизм); рефлексивное метапрограммирование — невозможным; показатели понимаемости, модифицируемости и тестируемости — слабыми (см. раздел Отсутствие возможностей). Реализация указателей на методы классов не стандартизирована, и их размер в различных компиляторах варьируется от диапазоне 4 до 20 байт, что значительно снижает портируемость программ с их использованием[36]. Принятая в сообществе С++ характерная методология декомпозиции задач приводит к проектным решениям, не доказуемым математически и неадкватным предметной области, и потому угрожающим неоправданными затратами и скрытыми ошибками. Традиционно в математике (и, соответственно, в более строгих языках программирования) понятие «класс» отождествляется с понятием «множества» (или реже «категории»). Понятие «наследования классов» в информатике традиционно означает создание «подклассов», то есть «подмножеств» или «подтипов» (по Карри, тип определяется как множество значений). В С++ эта традиция не соблюдается (в соответствии с принципом «Дать программисту свободу выбора, даже если это даст ему возможность выбирать неправильно» — см. раздел Философия C++[пояснения 5]), и наследование зачастую используется вместо вложения — то есть определение нового типа на основании более простых взаимно-ортогональных типов осуществляется посредством создания не совершенно нового типа, инкапсулирующего эти типы (и ортогонального им), а «их общего подтипа» (несмотря на то, что ортогональность означает отсутствие точек соприкосновения). Например, в [37] приводится учебно-рекомендательный пример реализации типа (класса) «список» как подтипа (подкласса) от «одного элемента этого самого списка», и базовый класс (надкласс) «элемент несуществующего списка», хотя и позиционируется как абстрактный скалярный тип (контейнер для некоего единственного значения), тем не менее содержит операции, присущие агрегатным типам и внешним интерфейсам (функции доступа к другим элементам несуществующего списка, внешним по отношению к текущему объекту), однако не может расцениваться как агрегатный тип и не предназначен для самостоятельного использования. Такое отношение типов является абсурдом с т.з. математики и, как следствие, невоспроизводимо на более строгих языках. Идеология некоторых библиотек также прямо опирается на возможность приведения типов вверх и вниз по иерархии классов (операции static_cast и dynamic_cast), подтверждая, что типобезопасность не входит в традиции языка. При множественном наследовании картина может быть ещё хуже. Ошибочность проектных решений, принятых в соответствии с этой идеологией, может обнаруживаться на поздних этапах разработки и из-за высокой вязкости требовать повторной разработки значительных частей проекта. Ярким примером является описанный в[15] случай:
Пример подобной проблемы описан в [C.Potts, "Software-Engineering Research Revisited, " IEEE Software (Sept., 1993)] и [M.Lubers, C.Potts & C.Richter, "Developing Initial OOA Models, " Proc. Intl. Conf. Software Eng., Los Alamitos, Calif. (1992)]. Для исследования применимости объектно-ориентированной декомпозиции к системам разного рода были проанализированы три случая. Один из них был реальным проектом разработки системы наведения ракеты Томагавк. Разработчики сочли, что все ракеты можно разделить на подклассы в соответствии с видом боеголовки и навигационными характеристиками. Также их можно разделить на тактические и учебные. Эти две таксономии были практически ортогональны, и было принято решение использовать множественное наследование для применения этих категорий к конкретным видам ракет. Однако, на более поздней стадии разработки было обнаружено, что ортогональность нарушена: хотя некоторые виды учебных ракет могут нести боеголовки, ядерные ракеты не могут. Продолжение принятого подхода к построению архитектуры привело бы к неэлегантным и искусственным моделям, скрывающим под массой деталей исключительные ситуации, которые легко упустить.

— Mаrtin Ward в [15]

Кроме того, в сообществе С++ культивировалось заблуждение, что инкапсуляция означает сокрытие данных. Многие языки разделяют эти понятия (например, в ML использование сигнатур позволяет гибко управлять сокрытием инкапсулированных в модуле определений; в Python возможность сокрытия вообще отсутствует, хотя механизмы инкапсуляции развиты лучше, чем в С++). Ян Джойнер критикует это заблуждение в С++:

Существует большая путаница относительно инкапсуляции, основным источником которой является С++, отождествляющий инкапсуляцию с сокрытием данных. Словарь Маккуори определяет глагол инкапсулировать как «помещать в прямом или переносном смысле в капсулу». Объектно-ориентированное понимание инкапсуляции заключается в том, чтобы помещать родственные данные, процедуры и определения в капсулу класса. Это не обязательно означает сокрытие. Сокрытие реализации является ортогональным понятием, которое становится возможным благодаря инкапсуляции. ... Инкапсуляция предоставляет возможность отделять абстрактный интерфейс класса от его реализации: интерфейс - это видимая поверхность капсулы, реализация скрыта в капсуле. ... Сокрытие реализации означает, что данными можно манипулировать, изменяя их, только внутри класса, но не означает сокрытие интерфейсных данных. ... Для сокрытия реализации в С++ необходимо осуществлять доступ к данным через функции Си. Это называется сокрытием данных в С++. ... Механизм доступа является скрываемой деталью реализации. С++ обеспечивает заметные различия в механизмах доступа к константам, переменным и функциям. ... Большинство не-Си-подобных языков обеспечивают единый механизм доступа к константам, переменным и процедурам, возвращающим значения.

— Ian Joyner в [20]

Результирующий ущерб от совокупности описанных свойств отмечается также Линусом Торвальдсом[мнения 4]: С++ провоцирует на написание в дополнение к структурам и функциям Си значительного объёма кода, не имеющего принципиального значения с т.з. функциональности программы.

Качество и культура программирования 
В ответ на объективную критику, сторонники C++ утверждают, что ничто не вынуждает программиста на C++ использовать «плохие» языковые средства, если он умеет использовать имеющиеся в языке и предназначенные для этой же цели «хорошие», и что программист на C++ имеет возможность выбора в соответствии с личными предпочтениями и уровнем знаний,— тогда как другие языки обычно предлагают чётко очерченный баланс между порогом вхождения и результативностью программиста. Проблема заключается именно в условии понимания программистом целей, для которых были предусмотрены те или иные возможности. Лозунг С++ «не навязывать „хороший“ стиль программирования» имеет достоинством лишь снижение порога вхождения; с т.з. качества ПО он является недостатком (см. типобезопасность) — при высоких требованиях качества (не только в аспекте надёжности) предпочтительными являются языки с семантикой, сводящей к минимуму риск влияния человеческого фактора, то есть именно навязывающие «хороший» стиль программирования (Ada, SML, Haskell, BitC) — но такие языки имеют более высокий порог вхождения. Однако, при анализе свойств С++ как инструмента разработки, низкий порог вхождения и ожидание высокой результативности заявляются его сторонниками одновременно, без противопоставления — это один из аспектов, в котором С++ претендует на «универсальную применимость» (см. Философия C++). Но тогда, поскольку все элементы семантики C++, отсутствующие в Си, были заимствованы из других языков (зачастую чужеродных Алголу),— очевидно, что для адекватного и аккуратного применения взаимно противоречивых возможностей и избегания провоцируемых ими ошибок, программист на С++ должен изначально знать непосредственно эти языки и лежащую в их основе формальную базу. Этого, однако, не наблюдается: практика показывает, что преимущественно положительная оценка и предпочтение использования C++ характерны лишь для тех, кто не владеет ни одним языком, не являющимся потомком Алгола (не считая ограниченного набора узко специализированных языков, таких как SQL, HTML, Perl и др. — зачастую даже не полных по Тьюрингу); программисты же более высокого уровня обычно отзываются о C++ далеко не так оптимистично и стараются его избегать, если нет вынужденности поддерживать legacy code[мнения 5]. Обобщение этих мнений даёт основания полагать, что существует обратно-пропорциональная зависимость между умением программировать на С++ в «хорошем» стиле и желанием использовать С++ в качестве инструмента разработки — из которой следует, что защищающие С++ программисты, по всей видимости, заблуждаются в отношении того, что именно следует считать «хорошим» стилем программирования. В частности, Линус Торвальдс говорит, что использует субъективное мнение кандидатов о С++ в качестве критерия отсева (предпочитающие язык С++ языку Си отвергаются)[мнения 4]. Таким образом, контр-аргументация сторонников С++ в аспекте качества и культуры программирования оказывается не убедительна и не перекрывает объективную критику. При этом, сама по себе попытка перевести объективную критику в разряд субъективной в сочетании с позиционированием С++ как «универсально применимого» языка, очевидно, являет собой попытку существенно снизить порог вхождения для самой профессии программирования — заявить, что высококвалифицированным в принципе может считаться программист, использующий единственный язык общего назначения для всех задач.
Исправление исправного 
Непрерывная эволюция языка побуждает (а порой вынуждает) программистов раз за разом изменять уже отлаженный код — это не только удорожает разработку, но и несёт риск внедрения в отлаженный код новых ошибок. В частности, хотя изначально обратная совместимость с Си была одним из базовых принципов С++, с 1999 года Си перестал быть подмножеством С++, так что отлаженный код на Си уже не может использоваться в проекте на С++ без изменений.
Сложность ради самой сложности 
Как уже отмечено в разделе Отсутствие возможностей, принцип С++ «не платишь за то, что не используешь» последовательно приводит к увеличению на порядки стоимости развития и поддержки продукта. Другими словами, пока программист на С++ «не платит за то, что он в С++ не использует», его работодатель переплачивает за использование языка С++ и программиста на нём. Это не является аргументом для сторонников С++ в пользу смены инструмента разработки — напротив, С++ определяется ими как «мощнейший» именно потому, что он изобилует опасными взаимно-противоречивыми возможностями, возлагая ответственность за доказательство их корректности на программиста. При кажущейся абсурдности такого определения, ему существует психологическое объяснение, данное Эриком Реймондом — это делает язык сам по себе почвой для личностного самоутверждения, облегчая возможность субъективно превращать процесс разработки из средства в самоцель:
Программисты — это зачастую яркие люди, которые гордятся … своей способностью справляться со сложностями и ловко обращаться с абстракциями. Часто они состязаются друг с другом, пытаясь выяснить, кто может создать «самые замысловатые и красивые сложности». … соперники полагают, что должны соревноваться с чужими «украшательствами» путём добавления собственных. Довольно скоро «массивная опухоль» становится индустриальным стандартом, и все используют большие, переполненные ошибками программы, которые не способны удовлетворить даже их создателей.

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

Эрик Реймонд в [38]

Саботаж 
Отмечены случаи, когда нерадивые программисты, пользуясь свойством сильной контекстной зависимости С++ и отсутствия возможности отслеживания макроопределений компилятором, тормозили разработку крупного проекта, написав буквально одну-две лишних, корректных с т.з. компилятора, строки кода, но внедрив за их счёт труднообнаружимую спонтанно проявляющуюся ошибку. Например:
#define if(a) if(rand())

#define j i
В языках с доказанной корректностью, даже с развитыми макро-средствами, нанести урон подобным образом невозможно.
Ненадёжность продукта 
Неоправданное обилие побочных эффектов (даже в таких простых операциях как индексация массива), в сочетании с отсутствием контроля со стороны системы времени исполнения языка и слабой системой типов, делает программы на С++ традиционно нестабильными, что исключает применение C++ при высоких требованиях отказоустойчивости. Кроме того, это увеличивает длительность самого процесса разработки[33]. Общеизвестные сообщения об ошибках при фатальных крахах прикладных программ, такие как «Access violation», «Pure virtual function call» или «Программа выполнила недопустимую операцию и будет закрыта», присущи в наибольшей степени именно С++. (Контр-примеры: для потомков ML фатальный крах в принципе невозможен — программа может навечно уйти в расчёты или выдать сообщение об ошибке, но никогда не обрушится — кроме того, контроль типов заметно сокращает время разработки[33]; семантика Форта обеспечивает практически гарантированное выявление ошибок на этапе разработки; Eiffel, Smalltalk и Erlang предоставляют простые способы обработки любых возможных ошибок самой программой без обрушения.)

Влияние и альтернативы[править | править исходный текст]

Ввиду изобилия критики в адрес С++, неоднократно предпринимались попытки предложить альтернативы C++, как для прикладного, так и для низкоуровневого программирования (не заявляемые как «универсально применимые» языки).

Одной из первых альтернатив в прикладном программировании стал язык Java, разработанный Sun, который часто ошибочно считают прямым потомком C++. На деле в Java от С++ нет ничего, кроме синтаксиса — семантика Java унаследована от языка Модула-2, и основы семантики C++ (такие как система типов, основанная на адресной арифметике, ручное управление памятью, умышленный отказ от фундаментального принципа ООП «всё — объект», и др.) в Java не прослеживаются. Семантика отдельных операторов также отличается: например, безусловный переход в Java отсутствует, а ключевое слово goto использовано для финализации (разновидности условного перехода). Кроме того, элементы семантики Си, унаследованные из Lisp (такие как функции высших порядков, функции с неограниченным числом аргументов и тернарная условная операция), в Java отсутствуют. Нет в Java и макроопределений времени компиляции. Всё это заметно меняет идеологию программирования на языке (хотя и сохраняет её в рамках общей идеологии потомков Алгола). Для улучшения коньюнктуры создатели Java выбрали синтаксис C++ и поощрили их сравнение в печати. Учитывая всё это, а также генеалогию языков (Modula-2 является потомком Симулы, как и С++, но им не является Си), Java правильнее называть «троюродным племянником» С++, нежели «наследником». По тому же пути пошла компания Microsoft, предложив язык C#.

После того, как было получено общественное признание Java и C#, были произведены попытки совмещения безопасности и скорости разработки, характерных для Java и C#, с эффективностью C++ — так появился язык D и диалект Managed C++, не получившие широкого признания. Одновременно с развитием Алголовского направления, сторонники функционального программирования и рефлексивного метапрограммирования предприняли попытку совместить модель типизации Хиндли-Милнера и макро-подмножество Common Lisp с языком C#, создав язык Nemerle. Его также зачастую стали рассматривать как «конкурент C++», что способствовало росту популярности функционального программирования и рефлексивного метапрограммирования[39] и побудило Microsoft дополнить базовый комплект своей среды разботки для C++ и C# (Visual Studio) компилятором языка F# — диалекта ML, адаптированного для совместимости с C#. Резонно полагать, что те же цели преследовали разработчики языков Scala и JavaScript — гибридных функционально-объектно-ориентированных языков, также унаследовавших синтаксис С++, но ориентированных на совместимость с Java.

Старейшим конкурентом С++ в задачах низкого уровня является Objective-C, совмещающий Си с объектной моделью Smalltalk. Из-за их соперничества долгое время полагалось, что эффективность и низкоуровневые возможности Си могут использоваться в сложно структурированных высокоуровневых задачах только посредством облачения их в ту или иную объектную модель. Однако, накопленные за многие годы достижения в сфере ФП позволили предложить альтернативный путь развития языка Си — совмещение его не с объектно-ориентированным, а с аппликативным программированием, то есть улучшение абстракции, строгости и модульности низкоуровневых программ посредством обеспечения не инкапсуляции изменяемого состояния, а наоборот, предсказуемости поведения и ссылочной прозрачности. Примерами работ в этом русле служат языки BitC, Cyclone и Limbo. Хотя есть и успешные попытки применения ФП в задачах реального времени без интеграции со средствами Си[40][41], всё же на данный момент (2013 г.) в низкоуровневой разработке применение в той или иной мере средств Си имеет лучшее соотношение трудоёмкости с результативностью. Много усилий было приложено разработчиками Python и Lua для обеспечения использования этих языков программистами на С++, так что из всех языков, достаточно тесно связанных с ФП, именно они чаще всего отмечаются в совместном использовании с С++ в одном проекте. Наиболее значимыми точками соприкосновения С++ с ФП можно считать привязки разработанных на С++ библиотек wxWidgets и Qt с характерной для С++ идеологией к языкам Lisp, Haskell и Python (в большинстве случаев привязки к функциональным языкам делают для библиотек, написанных на Си или на других функциональных языках).

Прямой потомок С++ на данный момент лишь один — язык D.

Сравнение Java и С++[править | править исходный текст]

Java и C++ часто сравниваются как языки, унаследовавшие синтаксис Си, несмотря на огромные различия на всех уровнях, от семантики до сферы применимости. Можно сказать, сравнение С++ с Java идёт вторым по частоте после сравнения С++ с Си.

Целевая ниша 
Java позиционируется для весьма конкретного сектора промышленности: безопасный язык с низким порогом вхождения для разработки прикладных пользовательских приложений широкого рынка с высокими показателями портируемости[42] — и с этой задачей справляется. С++ претендует на «универсальную применимость» во всех задачах для всех категорий программистов, но не удовлетворяет в полной мере требованиям ни одной из заявленных сфер применимости (см. раздел Критика).
Исполнение программы 
Java имеет формальную семантику, ориентированную на интерпретацию, но код Java компилируется в промежуточный код, который непосредственно перед запуском программы компилируется в машинный (иногда говорят об интерпретации байт-кода, но в данном случае это неверно — у современных наиболее распространённых сред исполнения Java оба этапа трансляции являются полностадийными, не ограничиваясь работой в рамках AST, с соответствующим сужением возможностей). C++ имеет естественную семантику, ориентированную на компиляцию, так что уже на аппаратной Java-машине был бы крайне неэффективен и ограничен по возможностям. Одно это определяет разницу в сферах применения языков: Java нецелесообразно использовать в низкоуровневом программировании; С++ — в разработке интернет-приложений. Механизм исполнения Java делает программы полностью портируемыми, по принципу «написано один раз — запускается везде (write once — run everywhere)», хотя это не было первостепенной целью разработчиков. Стандартное окружение и среда исполнения позволяют выполнять программы на Java на любой аппаратной платформе и в любой ОС без каких-либо изменений, при условии существования на данной ОС и платформе среды исполнения. Усилия по портированию программ минимальны, и могут быть сведены к нулю соблюдением определённых рекомендаций при разработке. Ценой портируемости в данном случае становятся определённые накладные расходы (например, размер среды исполнения Java превышает даже их размеры у всех функциональных языков).
Парадигма программирования 
Java в значительно более высокой степени, чем С++, отвечает фундаментальному принципу ООП «всё — объект» (но не в абсолютной — методы классов самостоятельными объектами не являются, в отличие от CLOS или Python). Для объявления глобальных функций или переменных в Java их необходимо оборачивать в фиктивные классы и назначать свойство статичности[43]. Для задания главной функции даже самой простой программы на Java необходимо поместить её в класс[44]. Программировать на Java в функциональном стиле затруднено[источник не указан 236 дней], поскольку на уровне синтаксиса языка нет поддержки основных элементов ФП (таких как функций высшего порядка) и нет средств макрорасширения языка.
Объектная модель 
Как и в С++, объектная модель Java наследуется из Симулы (в Java — через промежуточную ступень — язык Modula-2), то есть фундаментально отличается от оной в потомках языка Smalltalk (Objective-C, Python, Ruby). Но есть серьёзные отличия и от С++. В Java все методы являются виртуальными. Есть синтаксический сахар для определения абстрактных классов: использование ключевого слова interface делает все методы класса чистыми виртуальными — такие классы называются в Java «интерфейсами». Множественное наследование допустимо только для них, но не для обычных классов, что улучшает дисциплину программирования — на этапе реализации нет возможности нарушить структуру проекта, построенную на этапе архитектурного проектирования (в С++ это делается легко).
Операторы 
Безусловный переход в Java отсутствует. Тернарная условная операция в Java также отсутствует. Однако, большинство конструкций являются типичными для всех потомков Алгола: императивный порядок вычислений, присваивания, ветвления, циклы, инфиксные арифметические операции, аргументы в объявлениях и вызовах функций, и пр. В целом спецификация Java отвечает принципу наименьшего удивления и позволяет быстрый переход на Java с любого языка семейства Алгола. Однако, есть и исключения — например, Java, как и C++, позволяет пропускать break в ветви оператора switch[45].
Синтаксис 
Основные конструкции Java и характерное оформление блоков кода наследованы из Си; большинство синтаксических отличий обусловлены разницей в семантике. Спецификаторы видимости компонентов класса в Java указываются на каждый компонент, а не группами, как в С++. В Java нет механизма ввода синтаксического сахара в программу — перегрузки операторов.
Адресная арифметика 
C++ сохраняет возможность работы с низкоуровневыми указателями — это является причиной труднообнаруживаемых ошибок, но необходимо для низкоуровневого программирования. В Java адресной арифметики нет.
Кодогенерация 
C++ не только сохраняет препроцессор Си, но и дополняет его Тьюринг-полным языком шаблонов, существенно расширяя возможности автоматического построения кода. В Java макроопределения времени компиляции отсутствуют.
Интроспекция 
В C++ RTTI ограничена возможностью сравнивать типы объектов между собой и с буквальными значениями типов. В системе Java доступна более подробная информация о типах.
Управление ресурсами 
C++ позволяет использовать принцип «захват ресурсов путём инициализации» (RAII), при котором ресурсы ассоциированы с объектом и автоматически освобождаются при разрушении объекта (например, std::vector и std::ifstream). Также возможен подход, когда программист, выделяя ресурсы (память под объекты, открытые файлы и т. п.), обязан явно позаботиться о своевременном их освобождении. Java работает в среде со сборкой мусора, которая автоматически отслеживает прекращение использования объектов и освобождает занимаемую ими память, если в этом есть необходимость, в некоторый неопределённый момент времени. Ручное управление предпочтительнее в системном программировании, где требуется полный контроль над ресурсами, RAII и сборка мусора удобнее в прикладном программировании, поскольку в значительной степени освобождают программиста от необходимости отслеживать момент прекращения использования ресурсов. Сборщик мусора Java требует системных ресурсов, что снижает эффективность выполнения программ, лишает программы на Java детерминированности выполнения и способен следить только за памятью. Файлы, каналы, сокеты, объекты графического интерфейса программист на Java всегда освобождает явно.
Окружение 
в состав среды исполнения Java уже входят библиотеки для графики, графического интерфейса, доступа к базам данных и для прочих типовых задач, которые определяют стандарт де-факто их использования. Состав базовой библиотеки C++ предоставляет много меньше возможностей, с другой стороны предоставляя больше свободы в выборе сторонних библиотек.

См. также[править | править исходный текст]

Примечания[править | править исходный текст]

  1. Шилдт, 1998, с. 57
  2. Страуструп, 1999, 2.1. Что такое C++?, с. 57
  3. Стэнли Липпман, Pure C++: Hello, C++/CLI  (англ.)
  4. Страуструп, 1999, 1.4. Исторические замечания, с. 46
  5. Страуструп, 1999, 1.6
  6. Имя C+ не было использовано потому, что является синтаксической ошибкой в C и, кроме того, это имя было занято другим языком. Язык также не был назван D, поскольку «является расширением C и не пытается устранять проблемы путём удаления элементов C».
  7. Bjarne Stroustrup. C++ Glossary. Проверено 8 июня 2007. Архивировано из первоисточника 27 мая 2012.
  8. ISO/IEC 14882:1998, раздел 6.4, пункт 4: «The value of a condition that is an initialized declaration in a statement other than a switch statement is the value of the declared variable implicitly converted to bool … The value of a condition that is an expression is the value of the expression, implicitly converted to bool for statements other than switch; if that conversion is ill-formed, the program is ill-formed».
  9. STLport: Welcome!
  10. Интервью Б. Страуструпа LinuxWorld.com
  11. Интервью Б. Страуструпа журналу «Системный администратор»
  12. CNews: Эксклюзивное интервью с создателем языка программирования C++
  13. 1 2 Alan Kay's Definition Of Object Oriented Programming. Архивировано из первоисточника 13 августа 2013.
  14. 1 2 3 Mаrtin Ward Language Oriented Programming. — Computer Science Department, Science Labs, 1994.
  15. 1 2 3 Paul Hudak Modular Domain Specific Languages and Tools. — Department of Computer Science, Yale University.
  16. 1 2 Андрей Карпов 20 ловушек переноса Си++ - кода на 64-битную платформу. — RSDN Magazine #1-2007, 25.04.2007.
  17. Открытая переписка gmane.comp.version-control.git от 06.09.2007. Архивировано из первоисточника 13 августа 2013.
  18. 1 2 Страуструп, Дизайн и эволюция C++, 2007
  19. 1 2 3 4 Ian Joyner A Critique of C++ and Programming and Language Trends of the 1990s - 3rd Edition. — копирайт и список изданий.
  20. Петр Каньковски Альтернативные средства разработки для Windows. — RSDN Magazine #5-2003.
  21. MSDN: The switch statement in C#
  22. Walid Taha Domain-Specific Languages. — Department of Computer Science, Rice University.
  23. Lugovsky V.S. Using a hierarchy of Domain Specific Languages in complex software systems design. — 2008.
  24. Открытая переписка gmane.comp.version-control.git от 06.09.2007. Архивировано из первоисточника 13 августа 2013.
  25. Страуструп, Программирование: принципы и практика использования C++, 2001
  26. Открытая переписка gmane.comp.version-control.git от 06.09.2007. Архивировано из первоисточника 13 августа 2013.
  27. Dave Gottner. Templates Without Code Bloat. — Dr. Dobb's Journal, январь 1995.
  28. Adrian Stone. Minimizing Code Bloat: Redundant Template Instantiation. Game Angst (22 сентября 2009). Проверено 19 января 2010. Архивировано из первоисточника 27 мая 2012.
  29. Herb Sutter. C++ Conformance Roundup. — Dr. Dobb's Journal, январь 2001.
  30. Are there any compilers that implement all of this?. comp.std.c++ frequently asked questions / The C++ language. Comeau Computing (англ.) (10 декабря 2008). Проверено 19 января 2010. Архивировано из первоисточника 27 мая 2012.
  31. Scott Meyers. Code Bloat due to Templates. comp.lang.c++.moderated. Usenet (16 мая 2002). Проверено 19 января 2010.
  32. 1 2 3 Ray Tracer Language Comparison (бенчмарк языков программирования — ffconsultancy.com/languages/ray_tracer/)
  33. Boehm H. Advantages and Disadvantages of Conservative Garbage Collection.
    (ссылка изРеймонд, Эрик. Искусство программирования для Unix.. — 2005. — С. 357. — 544 с. — ISBN 5-8459-0791-8)
  34. Lutz Prechelt. An empirical comparison of C, C++, Java, Perl, Python, Rexx, and Tcl (англ.). Технологический институт Карлсруэ. Проверено 26 октября 2013.
  35. Don Clugston, CSG Solar Pty Ltd (Перевод: Денис Буличенко) Указатели на функции-члены и реализация самых быстрых делегатов на С++. — RSDN Magazine #6-2004.
  36. Шилдт, Теория и практика С++, 1996, с. 64-67
  37. Эрик Реймонд Искусство программирования для Unix = en:The Art of Unix. — Издательский дом "Вильямс", 2005. — ISBN 5-8459-0791-8
  38. Чистяков Влад aka VladD2 Синтаксический сахар или C++ vs. Nemerle :) // RSDN Magazine #1-2006. — 24.05.2006.
  39. Zhanyong Wan, Walid Taha. Архивировано из первоисточника 13 августа 2013., and Paul Hudak Event-Driven FRP // Department of Computer Science, Yale University.
  40. MLKit. Архивировано из первоисточника 1 августа 2013.
  41. 1.2 Design Goals of the Java™ Programming Language. Oracle (1 января 1999). Проверено 14 января 2013.
  42. Class Arrays, JavaTM 2 Platform Std. Ed. v1.4.2
  43. The Java Tutorials. A Closer Look at the «Hello World!» Application
  44. The Java Tutorials: The switch Statement
Мнения
  1. Programming Language Popularity (2009). Проверено 16 января 2009. Архивировано из первоисточника 27 мая 2012.
  2. TIOBE Programming Community Index (2009). Проверено 6 мая 2009. Архивировано из первоисточника 27 мая 2012.
  3. vanDooren. C++ keyword of the day: export. Blogs@MSMVPs (24 сентября 2008). — «The export keyword is a bit like the Higgs boson of C++. Theoretically it exists, it is described by the standard, and noone has seen it in the wild. … There is 1 C++ compiler front-end in the world which actually supports it»  Проверено 19 января 2010. Архивировано из первоисточника 27 мая 2012.
  4. 1 2 Открытая переписка gmane.comp.version-control.git от 06.09.2007. Архивировано из первоисточника 13 августа 2013.
  5. Наглядной демонстрацией служат многочисленные споры между программистами разного уровня квалификации, которые можно слышать повсеместно, например:
    * Форум программистов «Linux.ORG», тема «Чем так плох С++?» (msgid=1989166)
    * Форум программистов «Просто Трёп», тема «С++?» (bid=16&tid=466654)
    * Форум программистов «HardForum» (ранее «ProgZ.Ru»), тема № 45505 «Самый крутой язык программирования»
    * Форум программистов «dxdy», тема «Язык программирования для математика» (topic33965)
    * Форум программистов «RSDN», тема «Так в чем же принципиальные отличия ФП от ИП?» (decl/2473899)
Пояснения
  1. Описание в обычном синтаксисе, такое как «decltype(x+y) f(T x, T y)» недопустимо, так как оно нарушало бы общее правило, согласно которому все используемые в программе объекты должны быть объявлены до своего первого использования.
  2. Многие задачи предъявляют к языку особые требования — например, когнитивное моделирование требует единообразного представления кода и данных и наличие рефлексивности непосредственно как части системы типов — хотя языки, отвечающие этим требованиям, в свою очередь, накладывают определённые требования на вычислительные ресурсы, и оказываются неприменимы в ряде архитектур. Существуют и архитектуры, для которых неприменимы языки Си и С++ (например, AVR 1200). В подобных ситуациях единственная возможность «использовать» некий «универсальный» язык (такой как С++) состоит в написании интерпретатора языка, отвечающего требованиям задачи, решении на нём собственно задачи, и преподнесении всего кода как «решения на этом универсальном языке». Однако, подобная структура сама по себе отвергает тезис об «универсальной применимости» рассматриваемого языка, так как де-факто задача была решена на другом языке
  3. Например, Boost.Lambda позиционируется как лямбда-функция, однако С++ не воплощает лямбда-исчисление Чёрча целиком; имитация комбинаторов посредством языка шаблонов слишком затруднено, чтобы ожидать их использование на практике; продолжения, унаследованные от Си, считаются неидеоматичными и опасными, а их реализация посредством языка шаблонов невозможна; кроме того, применение лямбда-лифтинга для оптимизации С++ невозможно,— так что фактчески Boost.Lambda — это просто анонимная функция, а не объект лямбда-исчисления.
  4. Очевидно, что если сложность системы или элемента системы превышает порог возможностей человека по осознанию, то система или элемент системы не будут до конца осознаны. При этом трансляция высокоуровневых конструкций (даже всего лишь арифметических выражений) сама по себе является очень сложной задачей: она требует применения динамического программирования и/или большого числа эвристик; даже для эффективного выделения регистров под временные переменные в данном контексте требуется решить задачу раскраски ненаправленного ациклического графа в минимальное число цветов. Таким образом, необходимость решать задачу эффективной трансляции одновременно с полноценной и адекватной реализацией требуемого функционала повышает общую сложность разработки, по меньшей мере, по экспоненциальному закону относительно сложности реализации того же функционала на предметно-специфичном ЯСВУ, и делает само программирование NP-полным. Учитывая ограниченность способностей человека по восприятию и переработке информации — фактически невозможным, начиная с некоторого (вполне обозримого) порога сложности. Однако, методы трансляции хорошо формализуемы и могут быть воплощены в виде компилятора языка достаточно высокого уровня, обеспечивающего максимальную понимаемость кода, а использование такого языка человеком решает проблему сложности (см. M.Ward, Language Oriented Programming). Таким образом, для любого достаточно сложного алгоритма достаточно развитый компилятор произведёт заведомо более эффективный код, чем человек на языке низкого уровня; и для любых достаточно сложных условий всегда разумнее потратить часть средств на разработку компилятора высокоуровневого предметно-специфичного языка, чем писать на низкоуровневом языке общего назначения. Всё это хорошо известно в информатике (и именно этим обусловлено появление Фортана, а следом и языков более высокого уровня), но подвергается сомнению сторонниками императивной парадигмы программирования из-за чрезвычайно высокой сложности реализации оптимизирующих трансляторов императивными средствами и заведомой ограниченности возможностей оптимизации императивных языков. В функциональном программировании парсеры реализуются тривиально, а возможностей оптимизации существенно больше, поэтому языково-ориентированное проектирование сложных систем весьма распространено, и вынесение функциональности в код на Си для явного контроля за эффективностью осуществляется лишь при крайней необходимости.
  5. Как выразился Ж. Ж. Руссо, « Тысячи путей ведут к заблуждению; к истине — только один» — то есть вероятность совершения человеком неправильного выбора оказывается на порядки выше, чем правильного, если де-факто такая возможность ему предоставлена.

Литература[править | править исходный текст]

Ссылки[править | править исходный текст]