«Самая важная вещь в языке программирования - его имя. Язык не будет иметь успеха без хорошего имени. Я недавно придумал очень хорошее имя, теперь осталось изобрести подходящий язык.» Д. Э. Кнут
Эволюция C++09: флешбек
После первой спецификации C++, ратифицированной в 1998 году, был взят пятилетний перерыв, который позволил разработчикам компиляторов подстроиться под стандарт. Также такое время «радиомолчания» позволило Комитету получить отзывы относительно документа. В конце этого периода комитет стандартов ANSI выпустил обновлённую спецификацию, содержащую исправления и различного рода улучшения. Эти исправления были задокументированы в первом техническом списке ошибок от 2003 года.
Далее, члены Комитета начали принимать предложения по внесению изменений в C++. Данная инициатива была названа C++0x, так как ожидалось, что новая версия языка будет утверждена в первом десятилетии двадцать первого века. Но с течением времени стало очевидно: новую версию языка не получится ратифицировать ранее, чем в 2009 году, поэтому инициатива сменила название на C++09.
Разработка документа по библиотечным расширениям была запущена в 2004 году и завершена в январе 2005 года (она получила название TR1). В ней рекомендовалось внедрить несколько расширений в стандартную библиотеку C++, многие из которых пришли из фреймворка Boost. В апреле 2006 года Комитет принял все рекомендации TR1 (за исключением некоторых высокоуровневых математических библиотек, которые разработчикам компиляторов достаточно трудно имплементировать). В GCC 4.0 уже была реализована большая часть TR1 в пространстве имён std::tr1::. Помимо GCC, это сделали и разработчики Metrowerks CodeWarrior 9 и 10. Microsoft выпустили Visual C++ 2008 Feature Pack, который также реализует TR1 (спасибо wasker).
Комитет по разработке стандартов планирует завершить C++09 к концу 2007 года. В этом же году планируются две встречи в апреле и октябре. Если не возникнет никаких препятствий, то окончательная версия документа должна быть доступна в 2008 году, а её ратификация пройдёт где-то в 2009.
Философия C++09
После ратификации C++98 около десятилетия назад, Комитет по разработке стандартов не был заинтересован во внесении больших изменений в язык, но он поддерживал изменения, которые могли сделать язык более лёгким и доступным для изучения новичками. Многие программисты не желают быть экспертами конкретного языка программирования. Наоборот: они хотят быть профессионалами в своих областях и просто использовать C++ как средство реализации своих задач. Несмотря на добавление в язык новых мощных инструментов, главной целью всё равно остаётся его упрощение.
Ещё одна цель заключается в том, чтобы безболезненно обновить стандартную библиотеку перед сменой ядра самого языка. Изменения в ядре очень рискованы и могут привести к громадным проблемам совместимости. Напротив, улучшения библиотеки позволяют достичь великолепной гибкости при меньших рисках. Взять, к примеру, реализацию сборщика мусора: изменение ядра языка для самоочистки (как это сделано в Java и C#) приведёт к гораздо более серьёзным изменениям и потребует дополнительной обратной совместимости, а поддержка класса умного указателя (smart pointer) в стандартной библиотеке предоставляет программисту аналогичные возможности при меньшем размере затрат.
Наконец, Комитет старался улучшить реальную производительность везде, где это только возможно. Одна из сильнейших сторон C++ — это его производительность (относительно новых C# и Java). Именно поэтому многие программисты выбирают C++ своим основным языком программирования. В 2003 году, по сведениям IDC, насчитывалось около 3 миллионов программистов на C++, поэтому есть смысл совершенствовать язык для их удобства, а не пытаться превратить его в то, чем он не является.
Поучительная сказка о EC++
В 1999 году, консорциум разработчиков встраиваемых систем Японии (включая NEC, Hitachi, Fujitsu и Toshiba) предоставил предложение по выделению подмножества C++. Данное подмножество во многом повторяло бы C++, за исключением удаления некоторого числа особенностей языка, которые являлись, по мнению участников консорциума, очень сложными и наносили большой урон производительности. Основные составляющие языка, которые следовало бы удалить: множественное наследование, шаблоны, исключения, RTTI, новые операторы приведения и пространства имён. Такое подмножество языка называлось бы Embedded C++ (или просто EC++).
К удивлению членов консорциума, компиляторы EC++ не были быстрее компиляторов C++. Совсем наоборот: они были, в некоторых случаях, намного медленнее! Основатель C++, Бьярн Страуструп, объяснил, что шаблоны использовались в большей части стандартной библиотеки, и их удаление выглядело абсолютно непрактично. Одновременно с этим, консорциум объявил о том, что возможно появление расширенного (extended) EC++, который будет поддерживать шаблоны.
Когда расширенные EC++ компиляторы стали доступны, они опять были сопоставлены с их большими собратьями. К удивлению участников консорциума, прирост производительности по сравнению с C++ был незначительным. Отчасти проблема заключалась в том, что консорциум пренебрегал принципом C++ «вам не нужно платить за то, что вы не используете». После этого ISO отказался принимать какие-либо предложения касательно EC++.
В 2004 году Комитет C++0x, вдохновлённый фиаско EC++, постарался определить, какие возможности C++ действительно имеют большие проблемы с производительностью. Как выяснилось, существуют лишь три области, где производительность можно было бы действительно увеличить:
new и delete RTTI (typeid() и dynamic_cast<>) исключения (throw и catch)
Распределение памяти, как оказалось, оказывают наибольшее влияние на производительность, однако маловероятно, что вы стали бы использовать язык, который не распределяет память из кучи. Что касается RTTI и обработки исключений, у многих компиляторов есть переключатели, позволяющие их отключить. В современных компиляторах обработка исключений реализована на достаточно высоком уровне и проблема кроется лишь в RTTI. Во всяком случае, если придерживаться принципов С++, то удаление некоторых возможностей языка сопоставимо с их отключением.
Что касается EC++, Страуструп сказал: «По моему мнению, EC++ мёртв, но если даже нет, то он должен быть мёртвым».
Исправления и улучшения
Несмотря на то, что стандарт C++ в 1998 сам по себе был поразительным достижением, в нём было небольшое число проблем. Некоторые было трудно выловить, другие были известными проблемами, но очень часто их было недостаточно для создания новой резолюции. Бьярн Страуструп объяснил некоторые из них, например:
vector<vector<int>>xv; // Вполне возможно! vector<double> xv { 1.2, 2.3, 3.4 }; // Инициализация контейнеров STL stronger typing of enum's // Перечисляемые типы остаются в своей области видимости extern-ing of template's // Нет дублирования среди единиц трансляции
Если вы не знаете, почему возникают указанные выше ошибки, то лучше и не пытаться понимать причину их появления. Я буду рассматривать только первую ошибку. Недостаток заключается в том, что C++98 разбирает часть «>>» вектора как оператор сдвига вправо и генерирует ошибку. В C++09 эта ошибка будет исправлена. Вот небольшой пример:
template<int I> struct myX { static int const x = 2; } template< > struct myX<0> { typedef int x; } template<typename T> struct myY { static int const x = 3; } static int const x = 4; cout << (myY<myX<1>>::x>::x>::x) << endl; // C++98 выведет «3», а C++09 — «2»
Синхронизация с ANSI/ISO C99
Спустя год после ратификации C++, спецификация ANSI/ISO C была обновлена небольшим количеством изменений языка C. Многие из этих изменений уже имели место в C++, но они и в C также имели смысл. Другие, напротив, не были частью C++, но Комитет посчитал их ценными и попробует отразить их в спецификации C++09. К ним относятся:
__func__ // Возвращает имя функции, в которой находится long long // Расширенный встроенный тип, обычно используемый для 64-битных чисел int16_t, int32_t, intptr_t, и т.д. // Специфичные типы чисел double x = 0x1.F0 // 16-ричные числа с плавающей точкой Комплексные версии некоторых математических функций Макросы, принимающее нефиксированное число аргументов
Улучшения стандартной библиотеки C++
Стандартная библотека C++ (включая STL) — это большое количество полезных контейнеров и утилит. Несмотря на полноту её возможностей, существовал целый ряд компонентов, которые были так необходимы пользователю. С++09 восполняет эти пробелы следующими новыми библиотечными классами:
regex: ожидаемый всеми класс регулярных выражений array<>: одномерный массив, содержащий собственный размер (может быть 0) tuple<>: шаблонизированный класс кортежа Классы хеш-контейнеров STL: unordered_set<>, unordered_map<>
Разработчики, использующие GCC 4 (XCode 2.x), могут не ждать до 2009 года до подобных изменений в стандартной библиотеки, так как они могут использовать подобные расширения уже через std::tr1::.
Совершенствование потоков
Локальное хранилище:
thread int x = 1; // Глобально в рамках потока
Атомарные операции:
atomic { // Приостанавливает другие потоки в момент выполнения }
Паралелльное выполнение:
active { { ... } // Первый параллельный блок { ... } // Второй параллельный блок { ... } // Третий параллельный блок }
В случае с параллельным выполнением, вполне вероятно, что если компилятор посчитает, что в данном месте параллельные блоки будут лишь убыточны, он будет выполнять их просто подряд.
Ясно, что такие черты языка сильно упрощают разработку, которая в противном случае проводилась бы с использованием pthreads, мьютексов и т.п. Следует отметить, что вышеупомянутые характеристики до сих пор обсуждаются членами Комитета C++09, так что могут иметь место небольшие изменения.
Больше информации о подобных улучшениях можно получить в этом документе.
Шаблоны с различным количеством аргументов
Многие годы язык С позволял функциям иметь нефиксированное число параметров. К несчастью, этого было невозможно добиться с помощью C++98. В C++09 шаблоны могут иметь изменяемое количество типов. Вот самый просто пример:
//Выводит в stderr только тогда, когда флаг DEBUG установлен template <typename TypeArgs> void DebugMessage(TypeArgs... args) { #ifdef DEBUG //Реализация записи в stderr #else //Ничего не делать #endif } //Далее в коде DebugMessage("n is ", n); DebugMessage("x is ", x, " y is ", y, " z is ", z); DebugMessage("This is my trace: ", " time = ", clock(), " filename = " , __FILE__, " line number = ", __LINE__, " inside function: ", __func__);
Делегирование конструкторов
Другие языки, такие как C#, позволяют одному конструктору класса вызвать другой. В C++98 такая возможность отсутствовала, что вынуждало разработчика класса создавать отдельную функцию инициализации. В C++09 это становится возможным, как показано в коде ниже:
class MyClass { public: MyClass(); // Конструктор по умолчанию MyClass(void *myptr); // Получает указатель MyClass(int myvalue); // Получает числовое значение }; MyClass::MyClass(): MyClass(NULL) // Вызывает X(void *) { ... // Код } MyClass::MyClass(void *myptr): MyClass(0) // Вызывает X(int) { ... // Код } MyClass::MyClass(int myvalue) // Не делегируется { ... // Код }
NULL-указатели
В ANSI C NULL определён как (void *) 0. В C++ использовать NULL не рекомендуется. Почему? Потому что, в отличие от C, в C++ присваивать void-указатели указателям любого другого типа неправильно.
void *vPtr = NULL; // Правильно и в C, и в C++ int *viPtr = NULL; // Правильно в C, но неправильно в C++ // Нельзя присовить void * к int * в C++! int *viPtr = 0; // Правильно в C++
Тем не менее, распространение NULL в C++ коде очень велико, поэтому многие компиляторы просто генерируют предупреждение (не ошибку), когда происходит подобное присваивание. Другие переопределяют NULL в C++ как 0, таким образом предотвращая появления ошибки приведения. Несмотря на все «любезности» компиляторов, всё это сильно смущает начинающих программистов на C++.
void bar(int); // Получает целочисленное значение void bar(char *); // Получает char * bar(0); // Это указатель или просто число? bar(NULL); // Нет соответствующего прототипа
Таким образом, для упрощения использования пустых указателей, в C++09 вводится nullptr. Он может использоваться с указателями любых типов, но не может быть применён к встроенным типам.
char *cPtr1 = nullptr; // NULL-указатель в C++ char *vcPtr2 = 0; // Верно, но не рекомендуется int n = nullptr; // Неверно myX *xPtr = nullptr; // Может использовать с указателями любых типов void bar(int); // Получает целочисленное значение void bar(char *); // Получает char * bar(0); // Вызывает foo(int) bar(nullptr); // Вызывает foo(char *)
Ты где был, auto?
Когда язык C только разрабатывался, ключевое слово auto использовалось, чтобы сказать комплятору о расположени переменной на стеке, к примеру:
auto x; /* Переменная с именем x (целочисленная) расположена на стеке */
Когда ANSI C был ратифицирован в 1989 году, определение типа было удалено:
auto x; /* Неверно в ANSI C */ int x; /* Верно */ auto int x; /* Верно, правда излишне */
С того времени, auto стало ключевым словом в C (а позже и в C++), хотя практически никто не использовал auto с 1970-х. Спустя три десятка лет стандарт C++09 вновь вводит ключевое слово auto. Переменная, определённая под этим ключевым словом автоматически приобретёт тип при инциализации.
auto y = 10.0; // y — это число с плавающей точкой auto z = 10LL; // z — это long long const auto *p = &y; // p является const double *
Экономичность становится более очевидной при использовании вместе со сложными видами, как например ниже:
void *bar(const int doubleArray[64][16]); auto myFcnPtr = bar; // myFcnPtr теперь имеет тип "void *(const int(*)[16])"
В добавок ко всему, auto становится очень полезным для временных переменных, тип которых не так важен. Рассмотрим следующую функцию, которая проходится через элементы STL-контейнера:
void bar(vector<MySpace::MyClass *> x) { for (auto ptr = x.begin(); ptr != x.end(); ptr++) { ... //Различного рода код } }
Без ключевого слова auto тип для переменной ptr был бы vector<MySpace::MyClass *>::iterator. Кроме того, любое изменение в этом контейнере, например смена его с vector<> на list<>, или изменение имени класса, имени пространства имён непременно заставит программиста изменить определение переменной ptr, несмотря на то, что её тип абсолютно не важен в рамках цикла.
Что интересно отметить, в C# аналогичное поведение создаётся с помощью ключевого слова var.
Следует заметить, что инициализация всё ещё необходима для использования auto в C++09:
auto x; // Неверно в C++09
Но предположим, что вы знали, какой тип вам нужен (основываясь на другой переменной), но не хотели инициализировать? Новое ключевое слово decltype доступен для таких целей, как например в следующем куске кода:
bool SelectionSort(double data[256], double tolerance); bool BubbleSort(double data[256], double tolerance); bool QuikSort(double data[256], double tolerance); decltype(SelectionSort) mySortFcn; if (bUseSelectionSort) mySortFcn = SelectionSort; else if (bUseBubbleSort) mySortFcn = BubbleSort; else mySortFcn = QuikSort;
Умные указатели
Умные указатели — это объекты, которые могут сами понять время, когда следует удалить самих себя из памяти и не полагаться на программиста. Практически все современные языки, такие как Java и C#, управляют памятью в соответствии с подобной моделью, что позволяет избегать ненужных утечек памяти. В C++98 был небольшой подобный объект, auto_ptr<>. К сожалению, auto_ptr<> обладает некоторыми ограничениями, самое заметное из которых — это использование собственной модели распределения доступа. То есть последний auto_ptr<> был единственным владельцем памяти:
auto_ptr<int> ptr1(new int[1024]); auto_ptr<int> ptr2 = ptr1;
Из-за этого в сообществе C++ количество применений auto_ptr<> стремится к нулю.
Стандартная библиотека C++09 вводит более умный вид указателя: shared_ptr<>. Его главное отличие от auto_ptr<> состоит в том, что он использует распределённую модель прав и счётчик ссылок для определния времени освобождения памяти. Например:
main() { shared_ptr<int> ptr1; // Умный NULL-указатель ... { shared_ptr<int> ptr2(new int[1024]); ptr1 = ptr2; // Распределение владений (феодальных) } // ptr2 удалён, остался лишь ptr1 // Память до сих пор не освобождена } // ptr1 удалён, память освобождена
shared_ptr<> можно рассматривать как указатель, поэтому он может быть использован как *ptr, и может применяться в конструкциях, подобных ptr1->foo().
explicit shared_ptr<T>(T *ptr); // Присоединение к памяти shared_ptr<T>(T *ptr, Fcn delFcn); // Присоединение к памяти и пользовательская функция очищения shared_ptr<T>(shared_ptr<T> ptr); // Конструктор копирования shared_ptr<T>(auto_ptr<T> ptr); // Преобразование с auto_ptr<>
Стоит обратить внимание, что последний конструктор конвертирует данные из auto_ptr<> в shared_ptr<>, что значительно облегчает переход с предыдущих версий кода и обеспечивает обратную совместимость. Предоставляются ещё некоторые дополнительные функции, такие как swap(), static_pointer_cast() и dynamic_pointer_cast().
shared_ptr<> уже является частью пространства имён std::tr1:: и Mac-программисты могут использовать его через Xcode 2.x или выше.
Rvalue-ссылки
В языке C параметры функции всегда передаются по значению (by value), то есть передаётся копия параметра, но не актуальное его значение. Чтобы изменить переменную в C, функция должна передать указатель, как в примере:
void foo(int valueParameter, int *pointerParameter) { ++valueParameter; // Параметр передаётся по значению, модифицируется локальная копия ++pointerParameter; // Указатель передаётся по значению, но модифицируется всё равно локальная копия ++*pointerParameter; // Такое изменение остаётся постоянным }
Одной из мощнейщих возможностей C++ была передача параметров по ссылке (by reference) с использованием оператора «&». Это позволяло модифицировать данные параметра напрямую, без использования указателей.
void foo(int valueParameter, int &referenceParameter) { ++valueParameter; // Параметр передаётся по значению, модифицируется локальная копия ++referenceParameter; // Передача по ссылке, поэтому изменения остаются постоянными }
Ссылки должны быть применены к lvalues (так как это переменные, которые можно модифицировать), а не rvalues (которые доступны только для чтения).
int myIntA = 10; int myIntB = 20; foo(myIntA, myIntB); // myIntA = 10, myIntB = 21 foo(1, myIntA); // 1 передана по значению, myIntA = 11 foo(myIntA, 1); // Ошибка: 1 является rvalue и не может быть передан foo(0, myIntB + 1); // Ошибка: myIntB+1 является rvalue и не может быть передан
Иногда бывает полезно передать параметр по ссылке даже тогда, когда не нужно изменять его содержимое. Это особенно верное решение, когда большие классы или структуры передаются в функцию и следует избегать копирование столь громадных объектов.
void foo(BigClass valueParameter, const BigClass &constRefParameter) { ++valueParameter; // Передаётся по значению, изменения временны ++constRefParameter; // Ошибка: невозможно изменить константный параметр }
В C++09 вводится новый тип ссылки, который называется rvalue-ссылка (поэтому, всем знакомый тип ссылки в C++98 теперь будет называться как lvalue-ссылка). Rvalue-ссылки можно привязать к временным данным, но изменять их напрямую, без копирования. Оператор «&&» говорит о том, что ссылка является это rvalue-ссылкой:
void foo(int valueParameter, int &lvalRefParameter, int &&rvalRefParameter) { ++valueParameter; // Параметр передаётся по значению, все изменения локальны ++lvalRefParameter; // Lvalue-ссылка, все изменения постоянны ++rvalRefParameter; // Rvalue-ссылка, локальные изменения без необходимости создания копии } foo(0, myIntA, myIntB + 1); // Временное значение myIntB + 1 не копируется, но может быть передано
Одно из главных преимуществ rvalue-ссылок — это возможность воспользоваться преимуществами семантики перемещения, то есть перемещением данных из одной переменной в другую без копирования. Класс может определить конструктор перемещения вместо или вместе с конструктором копирования.
// Определение класса class X { public: X(); // Конструктор по умолчанию X(const X &x); // Конструктор копирования (lvalue-ссылка) X(X &&x); // Конструктор перемещения (rvalue-сылка) }; // Различные функции, возвращающие X X bar(); X x1; // Создание объекта x1 с использованием конструктора по умолчанию X x2(x1); // x2 становится копией x1 X x3(bar()); // bar() возвращает временное X, память перемещается прямо в x3
Первичная мотивация семантики перемещения — это увеличение производительнсти. Допустим, что у нас есть два вектора строк, данные между которыми мы хотим поменять местами. Используя стандартную логику копирования, мы получим следующий код:
void SwapData(vector<string> &v1, vector<string> &v2) { vector<string> temp = v1; // Новая копия v1 v1 = v2; // Новая копия v2 v2 = temp; // Новия копия temp };
Используя логику перемещения, мы получим примерно такой результат:
void SwapData(vector<string> &v1, vector<string> &v2) { vector<string> temp = (vector<string> &&) v1; // temp — те же данные, что v1 v1 = (vector<string> &&) v2; // v1 содержит v2 v2 = (vector<string> &&) temp; // v2 указывает на данные temp } // Не было произведено ни одного копирования, только перемещения!
Другие добавления в C++09
Кроме описанных возможностей, здесь представлен список некоторых других изменений:
Новые типы символов: chart16_t, char32_t
Статичные утверждения ( asserts, from Boost:: )
Связывание (aliasing) шаблонов
Проверка типов: is_pointer(), is_same()
Введение foreach
Новый генератор случайных чисел
Выводы
Многие изменения следующей версии C++ доступны программистам уже сейчас, благодаря тому, то что они касаются стандартной библиотеки. Несмотря на это, готовить себя к грядущему обновлению языка стоит уже сейчас. Читая про все эти изменения становится ясно, что C++ ждёт довольно интересное будущее.