Pull to refresh

C++‭ ‬2009

Reading time13 min
Views6.2K
«Самая важная вещь в языке программирования - его имя. 
Язык не будет иметь успеха без хорошего имени. 
Я недавно придумал очень хорошее имя, 
теперь осталось изобрести подходящий язык.»  
Д. Э. Кнут  


Эволюция 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++ ждёт довольно интересное будущее.
Tags:
Hubs:
+95
Comments148

Articles