Тёмные углы C и C++

от автора

>> Мопед не мой, публикуется от имени человека, которого нет на хабре. Соответственно, все вопросы ему на e-mail (либо инвайт, тогда сможет ответить в комментариях).

Привет, Хабрасообщество!

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

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

Думаю, вы со мной согласитесь, что C++ — язык с очень высоким порогом вхождения. Серьёзно! Я изучаю этот язык уже больше 3 лет, и практически каждую неделю открываю в нём что-то новое и удивительное. Именно об этом «новом и удивительном» и пойдёт речь в данной статье. Кому интересно — милости прошу под кат.

За то время, пока я изучал C++, у меня потихоньку накапливались интересные задачки, сниппеты и просто необычные куски кода, которыми я делился с коллегами по работе и знакомыми. У нас даже появилась своего рода традиция — каждый рабочий день с того момента, как я пришёл в компанию, я выкладывал по две новых задачки, которые мы старались по возможности обсудить в перерывах. Постепенно их набиралось всё больше и больше, пока, наконец, я не стал забывать некоторые из них. Именно в тот момент я решил предпринять попытку составления сборника так называемых «тёмных углов» C и C++. Я собрал воедино все те куски кода, что видел до сих пор, дополнил их цитатами из различных стандартов и продолжил копить свою «коллекцию». Изначально моей целью было собрать вместе всего 100 задачек, но я и оглянуться не успел, как их стало уже 200, 300 и вот теперь 400. На самом деле, их даже больше, но на данном этапе я решил ограничиться именно этим количеством.

Итак, представляю Вашему вниманию книгу «C and C++ Dark Corners». Конечно, назвать её книгой можно с натяжкой, ведь это, как я уже и сказал, сборник интересных и для кого-то малоизвестных мест C и С++.

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

В качестве примера приведу несколько вопросов из книги:

97. Что попадёт в stdout в результате выполнения след. кода и почему?

#include <iostream> #include <memory>  #include <boost/smart_ptr/scoped_ptr.hpp>  class Foo { public:  ~Foo() { std::cout << "Foo::~Foo() \n"; } };  class Bar : public Foo { public:  ~Bar() { std::cout << "Bar::~Bar() \n"; } };  class Baz : public Bar { public:  ~Baz() { std::cout << "Baz::~Baz() \n"; } };  int main() {  std::cout << 1 << '\n';  {   Foo* instance = new Baz;   delete instance;  }   std::cout << 2 << '\n';  {   std::shared_ptr<Foo> instance(new Baz);  }   std::cout << 3 << '\n';  {   std::shared_ptr<Foo> instance(false ? new Bar : new Baz);  }   std::cout << 4 << '\n';  {   boost::scoped_ptr<Foo> instance(new Baz);  }   std::cout << 5 << '\n';  {   std::unique_ptr<Foo> instance(new Baz);  }   std::cout << 6 << '\n';  {   std::auto_ptr<Foo> instance(new Baz);  } } 

A: Первый случай уже обсуждался ранее – здесь UB в чистом виде (как правило, это приводит к тому, что не будет вызван деструктор производного класса).

Второй случай выдаст на экран следующее:

Baz::~Baz()
Bar::~Bar()
Foo::~Foo()

Почему? Что произошло? Ведь мы же ясно видим, что деструкторы у данных классов не являются виртуальными. Или это один из частных случаев UB? На самом деле, тут всё вполне законно и должно работать так, как указано выше. Посмотрим в документацию к std::shared_ptr и boost::shared_ptr:

en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr

Proper delete expression corresponding to the supplied type is always selected, this is the reason why the constructors are implemented as templates using a separate parameter Y.

www.boost.org/doc/libs/1_51_0/libs/smart_ptr/shared_ptr.htm

This constructor has been changed to a template in order to remember the actual pointer type passed. The destructor will call delete with the same pointer, complete with its original type, even when T does not have a virtual destructor, or is void.

Третий случай выдаст:

Bar::~Bar()
Foo::~Foo()

Почему? Ведь мы только что обсуждали поведение std::shared_ptr! Что тут не так? Вспомните поведение тернарного оператора в C++ — он требует, чтобы второй и третий его операнды были одинакового типа. Более того, каст от базового к производному касту без лишних действий не выполнить, в отличие от обратной ситуации:

#include <iostream>  class Base { };  class Derived : public Base { };  int main() {  Base* first;  Derived* second;   Base* foo = second; // Ok  Derived* bar = first; // Error: invalid conversion from 'Base*' to 'Derived*' } 

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

#include <iostream> #include <typeinfo>  template <typename T, typename U> auto foo(bool b, T t, U u)-> decltype(b ? t : u) {  return b ? t : u; }  int main() {  auto _1 = foo(true, 0, 'c');  std::cout << typeid(_1).name() << '\n';  auto _2 = foo(false, 0, 'c');  std::cout << typeid(_2).name() << '\n'; } 

Переменные _1 и _2 будут одного и того же типа – int.

Именно поэтому Baz в данном случае будет приведён к Bar.

4, 5 и 6 случаи ничем не отличаются от первого – здесь UB. Надо помнить, что поведение std::shared_ptr отличается от остальных умных указателей.

43. Зачем может понадобиться писать так

#define DO_JOIN(FOO, BAR) DO_JOIN1(FOO, BAR) #define DO_JOIN1(FOO, BAR) FOO##BAR 

вместо

#define DO_JOIN(FOO, BAR) FOO##BAR 

A: Потому что препроцессор отработает конкатенацию не так, как того ожидал программист, в том случае, если в качестве аргумента(-ов) мы передадим в макрос DO_JOIN другой макрос:

#include <iostream>  #define DO_JOIN(FOO, BAR) DO_JOIN1(FOO, BAR) #define DO_JOIN1(FOO, BAR) FOO##BAR  #define MY_MACRO 5  int main() {  std::cout << DO_JOIN(1, MY_MACRO) << '\n'; } 

Output:

15

#include <iostream>  #define DO_JOIN(FOO, BAR) FOO##BAR  #define MY_MACRO 5  int main() {  std::cout << DO_JOIN(1, MY_MACRO) << '\n'; } 

error: unable to find numeric literal operator ‘operator"" MY_MACRO’

Получается, макрос MY_MACRO не успел раскрыться, в результате чего была попытка провести конкатенацию с 1 и MY_MACRO, что, разумеется, привело к ошибке.

ISO/IEC 14882:2011

16.3.1 Argument substitution [cpp.subst]

1 After the arguments for the invocation of a function-like macro have been identified, argument substitution
takes place. A parameter in the replacement list, unless preceded by a # or ## preprocessing token or
followed by a ## preprocessing token (see below), is replaced by the corresponding argument after all macros
contained therein have been expanded. Before being substituted, each argument’s preprocessing tokens are
completely macro replaced as if they formed the rest of the preprocessing file; no other preprocessing tokens
are available.

44. Что попадёт в stdout в результате выполнения след. кода?

#include <iostream> #include <stdexcept>  struct Foo {  Foo() { std::cout << "Foo::Foo()" << std::endl; }  ~Foo () { std::cout << "Foo::~Foo()" << std::endl; } };  void foo() {  Foo bar;  throw std::runtime_error("Error"); }  int main () {  foo(); } 

A: Зависит от реализации.

// 1

Foo::Foo()

// 2

Foo::Foo()
Foo::~Foo()

ISO/IEC 14882:2011

15.3 Handling an exception [except.handle]

9 If no matching handler is found, the function std::terminate() is called; whether or not the stack is
unwound before this call to std::terminate() is implementation-defined (15.5.1).

Например, в документации к gcc сказано, что раскрутки стека не произойдёт:

The stack is not unwound before std::terminate is called.

Сборник выкладывается на бесплатной основе, желающим помочь каким угодно образом буду безумно признателен и благодарен. По любым вопросам, рекомендациям и пожеланиям Вы можете обращаться на мой электронный ящик nikita.trophimov@gmail.com. Буду рад услышать абсолютно любое мнение по данному поводу.

В книге ещё много чего можно и даже нужно дорабатывать. Обещаю, что если Хабрасообщество проявит хоть какой-то интерес к данному проекту, я продолжу развивать его согласно Вашим предпочтениям и пожеланиям, дополнять новыми задачками и исправлять ошибки. У меня есть много мыслей по поводу улучшения «C and C++ Dark Corners» — среди них добавление тематических разделов, workaround’ы для различных компиляторов и т.д. Главное, чтобы этот интерес был не только с моей стороны.

Жду Ваших положительных и отрицательных отзывов, рекомендаций и, конечно, новых задачек.

Внимательно ознакомьтесь с тем, что указано во «введении», и приятного чтения!

ссылка на оригинал статьи http://habrahabr.ru/post/201236/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *