воскресенье, 8 апреля 2012 г.

new без скобок.

Возможно, для вас будет новостью, что в C++ можно писать как «new T()», так и «new T».

Можно сперва подумать, что это какой-то синтаксический сахар, и эти конструкции полностью идентичны. Это не так. Конечно, если явно определен конструктор T::T(), то, non c’è problema, вызовется именно он и разницы не будет никакой.

Если же явного конструктора по умолчанию не определено, то «new T()», в отличие от «new T», заполнит все поля объекта нулями. Если быть совсем точным, то в первом случае будет сделана «инициализация значением» (value-initialization), а во втором — «инициализация по умолчанию» (default-initialization), и то только для non-POD типа. Инициализация значением означает вызов конструкторов по умолчанию, если они явно определены пользователем, а если не определены — обнуление всех членов. Инициализация — это вызов конструкторов по умолчанию, какими бы они не были: сгенерированы компилятором или определены пользователем. При этом сгенерированные компилятором конструкторы ничего сами не обнуляют. Очевидно, инициализация по умолчанию будет быстрее. Подробнее о разных видах инициализации можно почитать в разделе 8.5.5 стандарта C++03 / С++11.

В более старом C++98 нет понятия value-initialization, поэтому «new T()» будет обнулять только POD-типы (если верить этому).

Кстати, та же самая штука со скобками справедлива не только для new, но и для локальных переменных на стеке. Например:

int i1; // i1 == garbage
Но вот так будет не совсем верно:
int i2(); // i2 == 0? wrong!
Переменная i2 трактуется как функция без аргументов, возвращающая int.

воскресенье, 1 апреля 2012 г.

Вся правда о виртуальных вызовах в конструкторах.


Что выведет такая программа?

class Base {
public:
  Base() {
    Init();
  }
  virtual void Init() {
    std::cout << "Base::Init()" << std::endl;
  }
};

class Derived : public Base {
public:
  virtual void Init() {
    std::cout << "Derived::Init()" << std::endl;
  }
};

int main() {
  Derived derived;
}

Функция Init() виртуальная. Значит, при вызове такой функции при конструировании объекта Derived будет задействована таблица виртуальных функций класса Derived, и вызовется Derived::Init().

Если у вас были подобные иллюзии, запустите этот код и окончательно развейте их.

Почему же так происходит?

Кто-то дает такое объяснение: когда выполняется конструктор Base, объект типа Derived еще не сконструирован, поэтому объект ссылается на таблицу виртуальных функий класса Base и вызывается метод базового класса. Это хорошее объяснение, но не совсем верное. Скорее, это описание реализации механизма виртуальных функций для конкретного компилятора.

На самом деле, причина такова: конструктор объекта типа Derived еще не отработал, иными словами, суперобъект не инициализирован, и вызывать какие-то его методы было бы опасно, так как они могут обратиться к неинициализированным данным. Поэтому в язык и ввели такое исключение для конструкторов (и деструкторов, кстати, тоже). В стандарте (от 2003 года) это описано в 12.7.3:

Member functions, including virtual functions (10.3), can be called during construction or destruction (12.6.2). When a virtual function is called directly or indirectly from a constructor (including from the mem-initializer for a data member) or from a destructor, and the object to which the call applies is the object under construction or destruction, the function called in the one defined in the constructor or destructor's own class or in one of its bases, but not a function overriding it in a class derived from the constructor or destructor's class, or overriding it in one of the other base classes of the most derived object (1.8).

Никакой технической проблемы с реализацией виртуальных вызовов из конструкторов никогда не было. Это не артефакт какого-то хитрого устройства механизма виртуальных функций. Как справедливо замечает Страуструп, «реализовать небезопасный вызов виртуальной функции из конструктора было бы гораздо проще». Главная мотивация этой «поправки для конструкторов» — обезопасить программиста, защитить дурака от самого себя.

Ну а чтобы обезопасить себя еще больше, следуйте девятому правилу Мейерса.

суббота, 24 марта 2012 г.

Куда «приклеивается» const?

При объявлении типа в C++ может быть не очевидно, к чему относится модификатор const. Например, что в точности означают такие объявления:

const int * p1;
int const * p2;
int * const p3;

И вообще, к чему относится const в общем случае?
Правило простое: const относится к тому, что стоит непосредственно слева от него; либо справа, если слева ничего нет.

Исходя из этого, p1 и p2 имеют одинаковые типы — указатель на константу.
В объявлении p3 const относится к *, то есть к указателю. Поэтому p3 — указатель-константа на readwrite-переменную.

Еще парочка примеров:
int const * const p4;  // константный указатель на константу
int * const * p5;  // указатель на константный указатель.

Ссылки по теме:









Понеслась

Очередная попытка завести блог про программирование. Буду сюда периодически постить про C++. Посмотрим, насколько меня хватит =)