В этой статье будет рассмотрен ряд багов в реализации стандарта С++11, которые имелись в Visual Studio 2012 и были исправлены в Visual Studio 2013. Таким образом мы теперь можем использовать С++11 так, как в теории предполагается его использовать. Если для обхода багов VS2012 вам пришлось понаписывать «костылей» — теперь их можно убрать.
К сожалению, не все баги были исправлены, кое-что мигрировало из VS2012 в VS2013, а также появились новые баги. Под катом вы найдёте детальный разбор текущего состояния дел.
Больше нет лимита на количество типов в variadic templates
Visual Studio 2013 поддерживает variadic templates в полном объёме, вещи вроде std::function или make_shared больше не имеют лимита на количество аргументов, которые они могут принять. В Visual Studio 2012 этот лимит существовал и был равен 5 (пяти).
Исправлены баги, касающиеся вывода типов
auto терял спецификатор выравнивания
Если вы использовали auto для объявления переменной типа, определяемого выражением __declspec(align(…)), align обрабатывался неверно в VS2012, что приводило к неправильному расположению данных в памяти и случайным крешам.
decltype можно было использовать вместо типа не во всех случаях
Несмотря на то, что тип, определяемый через decltype предположительно должно быть возможно использовать везде, где можно использовать обычные типы, VS2012 не позволяла написать, к примеру, вот такой код:
vector<int> a;
decltype(a)::iterator iter = a.end(); // ошибка в VS2012
declval приводил к ошибкам компиляции
Некоторый корректный согласно стандарту С++11 код не компилировался в VS2012 из-за неверной интерпретации declval.
Предположим, вы ходите объявить шаблон is_comparable:
template<typename, typename = true_type>
struct is_comparable : public false_type
{};
template<typenameT>
struct is_comparable<T,
typename is_convertible<decltype(declval<T>() > declval<T>()),
bool>::type> : public true_type
{};
Это не сработает в VS2012, поскольку declval не поймёт, что такое T.
Исправлены баги в умных указателях
Использование лямбда-функции в качестве custom deleter ломало преобразование к типу bool
Если вы использовали лямбда-функции для определение того, что должно случиться при удалении умного указателя, вы не могли использовать этот указатель в контексте преобразование его к типу bool:
auto stream_deleter = [](ofstream* os) { os->close(); };
unique_ptr<ofstream, decltype(stream_deleter)> p_log(&log_file, stream_deleter);
if (!p_log) // compile error
cout << "Couldn't open file" << endl;
Вызов unique_ptr::reset мог привести к двойному удалению
Порядок операций в методе reset не соответствовал порядку, описываему стандартом. Это могло приводить к двойному удалению объекта. Пример:
class SelfReferential
{
unique_ptr<SelfReferential>& _p_self;
public:
SelfReferential(unique_ptr<SelfReferential>& p) : _p_self(p)
{}
~SelfReferential()
{
_p_self.reset();
}
};
unique_ptr<SelfReferential> p;
p = unique_ptr<SelfReferential>(new SelfReferential(p));
p.reset(); // двойное удаление через ~SelfReferential
Вызов метода reset запускает деструктор SelfReferential, который снова вызвает reset. Двойное удаление происходит потому, что метод reset сбрасывает указатель на подконтрольный объект после его удаления, а не до него.
shared_ptr, protected-деструктор и nullptr
Вы не могли создать shared_ptr для класса с protected-деструктором, инициализировав его nullptr:
class Interface
{
public:
virtual void do_stuff() = 0;
protected:
~Interface() {}
};
class Implementation : public Interface
{
public:
void do_stuff() override
{
// ...
}
};
shared_ptr<Interface> ptr1 = make_shared<Implementation>(); // OK
shared_ptr<Interface> ptr2 = nullptr; // ошибка
В Visual Studio 2013 этот код компилируется, как и предполагается стандартом.
Исправлены баги в библиотеке type traits
Неправильная работа is_function
is_function возвращает неверный результат, если переданная ей функция содержит слишком много аргументов:
typedef void f(int, bool, int*, int[], int, int, int, int, int, int, int);
is_function<f>::value; // false, хотя должно быть true
Также результат ошибочен для функций с calling convention, отличным от дефолтного
Аналогично, is_member_function_pointer не удаётся верно вернуть результат для методов с явно заданным calling convention
is_member_pointer, наоборот, не верно работает с __cdecl-методом:
typedef void (__cdecl A::*ccall_proc)(int, long, double);
is_member_pointer<ccall_proc>::value; // false, но должно быть true
is_object был определен через is_function, таким образом вышеуказанная ошибка с большим количеством аргументом функции распространяется и на него, приводя к неверному определению объекта.
is_scalar не распознавал nullptr_t
is_scalar<nullptr_t> ошибочно возвращал false в VS2012 – стандарт определяет nullptr_t как скалярный тип.
is_pod неверно понимал void
is_pod ошибочно возвращал true в VS2012, хотя void не является POD-типом
is_constructible возвращал неверные результаты для ссылок
is_constructible вёл себя неверно со ссылочными типами, возвращая false для вещей типа:
is_constructible<const string&, string>::value;
is_constructible<const string&, string&&>::value;
Баги в alignment_of и aligned_union
alignment_of в VS2012 генерирует generatee a ложные предупреждения о недоступном деструкторе, если вы используете его на типе с приватным деструктором.
Кроме того, aligned_union работал неверно в VS2012:
typedef aligned_union<16, string>::type StorageType;
sizeof(string); // 24
sizeof(StorageType); // 16, но должно быть 24 или больше
aligned_union должен иметь статический член alignment_value, содержащий значения выравнивания для шаблонных аргументов T1, …, Tn. Это, однако, не было реализовано в VS2012.
common_type ошибочно возвращает void
Вместо ошибки компиляции, как это предполагается по стандарту, common_type возвращал void в VS2012:
common_type<int, string>::type; // void
common_type также возвращает void ошибочно для пользовательских типов, когда преобразование *is* возможно:
struct A {};
struct AWrapper {
AWrapper() {}
AWrapper(const A&) {}
};
common_type<A, AWrapper>::type; // void
result_of не компилируется в некоторых случаях
Если вы решите использовать move-only аргумент с этим шаблоном в VS2012, у вас будут неприятности:
result_of<Copyable(MoveOnly&&)>::type; // ошибка компиляции
Исправлены ошибки в STL-контейнерах и алгоритмах
minmax_element не работал
Стандартом определяется две версии этого алгоритма:
pair<Iter, Iter> minmax_element(Iter first, Iter last)
pair<Iter, Iter> minmax_element(Iter first, Iter last, Compare comp)
Они должны возвращать (first, last), где first указывает на наименьший элемент, а last — на наибольший, или make_pair(first, first) — если диапазон пуст. В VS2012, однако, вместо этого возвращалось make_pair(min_element(first, last), max_element(first, last)).
Контейнеры ошибочно требовали от типов элементов обязательного наличия move-конструкторов
Все move-конструкторы контейнеров ошибочно требовали от типа элемента наличия move-конструктора.
struct A
{
A() {}
private:
A(A&&);
A(const A&);
};
deque<A> source;
deque<A> target(move(source)); // ошибка компиляции
Аналогично, операторы доступа к элементам map и unordered_map требовали обязательного наличия move-конструкторов:
map<string, A> m;
A& elem = m["abc"]; // ошибка компиляции
Исправлены ошибки, связанные с параллелизацией и асинхронностью
shared_future создаваемая из future
Ещё один баг в VS2012 был в реализации future и shared_future для ссылочных типов и void. Этот баг позволял следующему коду скомпилироваться (что явно ошибочно, поскольку future — это move-only тип):
future<int&> f_ref;
shared_future<int&> sf_ref(f_ref); // компилируется, хотя и не должно
future<void> f_void;
shared_future<void> sf_void(f_void); // компилируется, хотя и не должно
Утечка памяти в классе thread
Баг, который мог приводить к утечкам памяти при завершении программы. Это случалось потому, что поток создавал, но никогда не уничтожал объект at_thread_exit_mutex, а так же некоторые внутренние структуры данных.
Бесполезные функции wait в future, полученной от promise
Из-за бага в Visual Studio 2012, функции wait_for и wait_until таких future-объетов возвращали future_status::deferred вместо of future_status::timeout or future_status::ready, делая эти методы бесполезными.
Неверные сообщения в future_error исключениях
Баг несоответствия кода ошибки и её описания, из-за чего, например, получив исключение «broken promise», сообщение будет содержать текст «future already retrieved». Правильным был только код ошибки.
atomic-шаблон не мог быть определен для типа без конструктора по-умолчанию
Вы получали сообщение об ошибки при попытке использования atomic-шаблона для типа без конструктора по-умолчания, хотя это и не верно.
atomics работали медленно
В VS2012 atomic-операции иногда перебарщивали с проверками целостности (делали их там, где это не обязательно). Хотя это и не нарушает стандарта, код работал медленнее, чем мог бы. VS2013 имеет совершенно новую реализацию atomic-операций, которая работает значительно быстрее.
Исправлены ошибки в генерации случайных чисел
В дебаг-режиме mersenne_twister_engine генерировал ошибочный assert, если вы пытались инициализировать его нулём.
Потоковый оператор для subtract_with_carry_engine содержал ошибку, приводящую к неопределенному поведению.
independent_bits_engine и shuffle_order_engine не инициализировали внутренние члены в своих конструкторах перемещения, что иногда приводило к бесконечным циклам.
Исправлены ошибки в библиотеке рациональной арифметики
В библиотеке было найдено несколько багов:
Вы не могли написать следующий код в VS2012
ratio_add<ratio<1, 2>, ratio<1, 3>>::num;
ratio_add<ratio<1, 2>, ratio<1, 3>>::den;
Вместо этого вы были вынуждены обращаться к числитею и знаменателю через их тип:
ratio_add<ratio<1, 2>, ratio<1, 3>>::type::den;
ratio_add<ratio<1, 2>, ratio<1, 3>>::type::num;
Ещё одна ошибка была в реализации сравнения.
cout << "2/60 < -1/3: " << ratio_less<r2_60, r1_3>::value << endl; // false
cout << "2/60 < 1/-3: " << ratio_less<r2_60, ratio<1, -3>>::value << endl; // true но должно быть false
Таким образом в VS2012 вы должны были быть уверенными, что знаменатель, переданный в шаблон — всегда положительное число.
Ещё один баг был в том, что ratio_equal правильно определял неравенство, но не всегда правильно определял равенство:
ratio_equal<ratio<1, 4>, ratio<4, 16>>::value; // false но должно быть true
И вот ещё одни баг. Когда у вас есть ratio<N, D>, если D — ноль или число, превышающее intmax_t — ваша программ однозначно невалидна. Visual Studio 2012, однако, не определяла такие ошибки:
typedef ratio<1, 0> r_error;
cout << r_error::den << endl; // не должно компилироваться, но компилируется
typedef ratio<INTMAX_MIN, 1> r_error2;
cout << r_error2::num << endl; // не должно компилироваться, но компилируется
В реализации Visual Studio, static_assert утверждения, которые должны бы были срабатывать в этих ситуациях, размещены в конструкторе ratio. Но конструктор срабатывает только при создании объекта класса, а в примерах выше этого не происходит.
Аналогично, некоторый код компилируется, хотя и не должен:
// компилируется, хотя и не должен - с предупреждением о переполнении
ratio_multiply<ratio<1, INTMAX_MAX>, ratio<1, 2>>::type;
Другие ошибки в Visual Studio 2013
В tuple_element не делается проверка выхода за границы
tuple_element<I, array<T, N>> должен проверят, что I < N и не компилироваться, если это не так. Этого не происходило до VS2013.
Неверное преобразование к bool для std::function
В некоторых случаях преобразование могло давать неверный результат в VS2012, поскольку объект не был пуст, когда по идее должен быть им:
// JetPlane наследуется от Plane
function<bool(JetPlane*)> plane_ready_func = function<bool(Plane*)>();
if (plane_ready_func) // должно быть false, но нет
{
plane_ready_func(nullptr); // вызывается и бросает bad_function_call
}
Присваивание для rvalues
Visual Studio 2012 не запрещает присваивание для rvalues, как это определено стандартом:
struct Dummy
{
int _x;
};
Dummy get_dummy()
{
Dummy d = { 10 };
return d;
}
get_dummy()._x = 20; // компилируется, хотя и не должно
align() неверно обновляет out-параметры
Функция корректно рассчитывает возвращаемый адресс, но неверно обновляет два последних параметра:
void* p = (void*)0x1;
// пытаемся выровнять 200 байт по границе в 32 байта
// при общем доступном объёме в 230 байт
size_t space = 230;
void* res = align(32, 200, (void*&)p, space);
// res равен null поскольку 31 байт необходим для выравнивания,
// но в наличии только 30
space = 256; // увеличиваем общий объём до 256
res = align(32, 200, (void*&)p, space);
// res теперь 0x20 (выровнян до границы в 32)
// p равен 0xE8 (200 + 32) но должен быть 0x20
// space равен 25 но должен біть 225
time_put не работает с wchar_t
time_put не генерирует вывода когда инициализирован wchar_t.
Заключение
Полный список изменений в VS2013 (не только в C++11) вы можете прочесть вот в этом посте, написанном Stephan Lavavej.
В дополнение к реализации новых возможностей С++11, в Visual Studio 2013 исправлено множество баков в существующей функциональности компилятора и библиотек, от некорректных ошибок компилятора до утечек памяти и низкой производительности. Это однозначно положительная динамика.
К сожалению, VS2013 всё ещё содержит некоторое количество багов, доставшееся по наследству от VS2012 и добавляет некоторые новые. Обо всём этом я сейчас пишу книгу, она ещё не закончена, но кое-что вы можете прочесть уже сейчас.
Автор: tangro