Сообщений 0    Оценка 118        Оценить  
Система Orphus

C++ Q&A

Автор: Павел Кузнецов
Источник: RSDN Magazine #5-2004
Опубликовано: 27.12.2002
Исправлено: 10.12.2016
Версия текста: 1.0
Q. Бывают ли в C++ чисто виртуальные деструкторы? Кажется мне, что нет – ведь деструктор наследника обязан вызвать деструктор базового класса. А что ему делать, если у базового класса деструктор не определен?
Q. Является ли в C++ имя функции указателем на ее начало, как это было в С?

Q. Бывают ли в C++ чисто виртуальные деструкторы? Кажется мне, что нет – ведь деструктор наследника обязан вызвать деструктор базового класса. А что ему делать, если у базового класса деструктор не определен?

A. Чисто виртуальные деструкторы бывают. В этом отношении деструкторы ведут себя так же, как и любые другие функции, за исключением того, что деструктор базового класса вызывается деструктором потомка вне зависимости от желания программиста.

        virtual ~Base() = 0 {};

Этот вариант выглядит, действительно, немного "странновато". Но не потому, что есть определение чисто виртуальной функции, а потому что это определение совмещено с ее объявлением. Текущая версия стандарта подобного не разрешает. То есть чисто виртуальные функции можно определять только вне тела класса, Но "чистая виртуальность" и наличие определения – вовсе не взаимоисключающие вещи. Чисто виртуальной функцию делают для того, чтобы гарантировать, что

  1. она будет обязательно реализована в одном из наследников;
  2. класс будет абстрактным.

Чисто виртуальными деструкторы обычно делают именно для второго случая. Например, в классе больше виртуальных функций нет, но хочется, чтобы класс был абстрактным.

Любая виртуальная функция, в том числе и чисто виртуальная, может быть вызвана не виртуально. Это означает, что при желании чисто виртуальные функции можно определять, но вызвать их можно будет только не виртуально. В случае деструктора этот не виртуальный вызов генерируется компилятором в деструкторах классов-наследников, поэтому даже чисто виртуальные деструкторы обязательно должны быть определены. Например:

        class C
{
public:
  virtualvoid f();
  virtualvoid g() = 0;
  virtual ~C() = 0;
};

void C::f()
{
}

void C::g()
{
}

void C::~C()
{
}

class D : public C
{
public:
  void f();
  void g();
  ~D();
};

void D::f()
{
  C::f(); // невиртуальный вызов C::f
}

void D::g()
{
  C::g(); // невиртуальный вызов C::g, значит чисто виртуальная C::g должна быть определена
}

void D::~D()
{
  // неявный невиртуальный вызов C::~C
}

Q. Является ли в C++ имя функции указателем на ее начало, как это было в С?

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

        bool foo();

void bar()
{
  typedefbool F();
  // ссылка на функцию, имя функции к указателю здесь не приводится
  F& f = foo; 
}

Ссылка на функцию может быть неявно приведена к указателю на функцию:

F* pf = f; // указатель на функцию

Но указатель можно получить и явно:

F* pf = &f;

Естественно, все то же самое может быть проделано и с использованием непосредственно имени функции.

Еще раз подчеркиваю, что эта функциональность для "обычных" функций была оставлена только из соображений совместимости с C, т.к. у этого правила есть некоторые отрицательные моменты. Например, вместо:

        if (foo())
   ...

можно легко написать:

        if (foo)
   ...

что будет корректной с точки зрения языка конструкцией, но вряд ли будет соответствовать ожиданиям программиста, так как во втором случае происходит не вызов функции, а проверка ее на NULL, что бессмысленно для имени функции. В этом частном случае большинство (если не все) компиляторов выдаст предупреждение, что сводит реальный вред от подобных вещей, фактически, к нулю.

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

        void baz(...);
baz(foo());

можно запросто написать:

        void baz(...);
baz(foo);

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

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

Таким образом, выражения, обозначающие функцию-член класса, неявно не приводятся к указателю на функцию-член. Более того, для получения указателя на член недостаточно использования просто имени функции-члена, без явной квалификации идентификатором класса. И еще: использование скобок в сочетании с операцией & : &(ClassId::member) — к получению указателя на член тоже не приводит.

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

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

Тут же становится очевидной разумность требования явного указания операции &, т.к. синтаксис ClassId::member уже зарезервирован для явной квалификации класса, к переменной-члену которого осуществляется доступ:

        struct B
{
   int i;
};

struct D : B
{
  double i;

  D()
  {
    B::i = 10;
  }
};

Также очевидно, что желательно, чтобы в контексте класса B выражение &i, как и везде, обозначало получение "обычного" указателя на переменную i. Кроме того, вне класса B, выражение &B::i уже обозначает получение указателя на член B::i.

Поэтому, для того, чтобы отличать в контексте класса B создание указателя на член int B::* от получения "обычного" указателя int*, вполне естественно потребовать всегда, вне зависимости от контекста, явно указывать класс, указатель на поля которого требуется получить.

Представим теперь, что мы захотели в контексте класса D получить "обычный" указатель int* на переменную B::i. Учитывая значение выражения B::i в контексте класса D, вполне естественным был бы синтаксис &B::i, но он приводит к неоднозначности с операцией формирования указателя на член int B::*. В общем, такое столкновение "интересов" не критично, т.к. потребность получения "обычного" указателя на переменную-член базового класса возникает относительно редко.

Тем не менее, для устранения неоднозначности, было принято решение, что выражение вида &ClassId::member всегда означает формирование указателя на член, а выражение &(ClassId::member) — получение "обычного" указателя. Последнее для нестатических переменных-членов, в принципе, эквивалентно выражению &this->ClassId::member.

Возвращаясь к указателям на функции-члены, вряд ли было бы удобным, если бы cемантика применения к функциям-членам операции & отличалось от аналогичных действий с членами-данными.

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


Эта статья опубликована в журнале RSDN Magazine #5-2004. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 118        Оценить