понедельник, 2 сентября 2013 г.

Вызов конструкторов для статических элементов шаблонного класса

Всем известно, что static элементы класса создаются как нелокальные переменные:

9.4.1.6
Static data members are initialized and destroyed exactly like non-local variables (3.6.2, 3.6.3).

но при работе со static элементами шаблонного класса все не так очевидно.

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

struct Static
{
        Static(){ cout << "Static\n"; }
};

template<typename T>
struct StaticConstructTemp
{
        static Static m_static;

        void init()
        {
            m_static;
        }
};

template<typename T>
Static StaticConstructTemp<T>::m_static;

int main()
{
        StaticConstructTemp<int> test;    // Инстанцирование класса не приводит к вызову конструкторов static элементов-объектов.
test.init();                      // Конструктор вызывается только тут.
        return 0;
}


- Вызов конструктора для статического элемента  не шаблонного класса происходит до первого обращения к нему (в момент запуска программы) по обычным правилам.

struct Static
{
        Static(){ cout << "Static\n"; }
};


struct StaticConstruct
{
        static Static m_static;

        void init()
        {
             m_static;
        }
};

Static StaticConstruct::m_static;


//Обращение не требуется, конструктор вызывается.

В принципе, это нельзя назвать проблемой, но если требуется выполнять некоторую инициализацию, зависящую от создания данного static объекта, то ее отложение на неопределенный срок может быть чревато.

воскресенье, 1 сентября 2013 г.

Поиск имени в шаблонных базовых классах

В классовых иерархиях поиск имен в шаблонных базовых классах не происходит.  
Есть 3 способа заставить компилятор выполнять поиск имен в шаблонных базовых классах:
1. Обращаться к именам посредством указателя this;
2. Ввести имена базовых классов с помощью using-объявления;
3. Выполнять полную квалификацию имени при обращении, но это отключает механизм динамического связывания;

В GCC поиск в шаблонных базовых классах не выполняется согласно стандарту.
В MSVC поиск в шаблонных базовых классах происходит и ошибка не выдается.

Пример:

template<typename T>
class Base
{
public:
        T value;
        Base() : value() {}
        virtual ~Base()  {}

        void f()         { cout << "Base::f\n";                    }
        virtual void v() { cout << "virtual Base::v\n";            }

        virtual void call() { cout << "Base::call\n";                      }
};

template<typename T>
class Derived : public Base<T>
{
        virtual void v() { cout << "virtual Derived::v\n"; }
public:
        // По стандарту код ф-ии call должен приводить к ошибке компиляции,
        // но в MSVC поиск в шаблонных базовых классах происходит и ошибки нет.
        virtual void call()
        {
                 cout << ">>> Derived: value == " << value << "\n"; // Не должен находить value.
                 f();                     // Не должен находить f.
                 v();
        }
};

template<typename T>
class Derived_this : public Base<T>
{
        virtual void v() { cout << "virtual Derived_this::v\n"; }
public:
        virtual void call()      //  this включает поиск.
        {
                 cout << ">>> Derived_this: value == " << this->value << "\n";
                 this->f();
                 this->v();
        }
};

template<typename T>
class Derived_using : public Base<T>
{
        // using имен базового класса делает их видимыми.
        using Base<T>::value;
        using Base<T>::f;
        using Base<T>::v;
       
public:
        virtual void v() { cout << "virtual Derived_using::v\n"; }
        virtual void call()
        {
                 cout << ">>> Derived_using: value == " << value << "\n";
                 f();
                 v();
        }
};

template<typename T>
class Derived_explicit_call : public Base<T>
{
        virtual void v() { cout << "virtual Derived_explicit_call::v\n"; }
public:
        // Явный вызов с полной квалификацией имени выполняет поиск в базовом
        // классе, но отключает динамическое связывание.
        virtual void call()
        {
                 cout << ">>> Derived_explicit_call: value == " << Base<T>::value << "\n";
                 Base<T>::f();
                 Base<T>::v();
        }
};


int main()
{
        Base<int>* pb            = new Derived<int>;
        Base<int>* pb_this                = new Derived_this<int>;
        Base<int>* pb_using      = new Derived_using<int>;
        Base<int>* pb_expl_call = new Derived_explicit_call<int>;

        pb->call();
        pb_this->call();
        pb_using->call();
        pb_expl_call->call();

        delete pb;
        delete pb_this;
        delete pb_using;
        delete pb_expl_call;
        return 0;

}

Источник: Скотт Мэйерс Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.

По мотивам правила 43.

Ключевое слово typename

Имена в шаблоне, которые зависят от параметра шаблона, называются зависимыми именами. Зависимое имя внутри класса
называется  вложенным зависимым именем.

-В объявлениях параметров шаблона ключевые слова class и typename взаимозаменяемы.
-Используйте typename для идентификации вложенных зависимых имен типов, если они не встречаются в списке базовых классов или в качестве идентификатора базового класса в списках инициализации членов.
Причина: для разрешения неоднозначности, т.к. в теле функции шаблона вложенное зависимое имя может быть именем типа или статического элемента. По умолчанию анализатор воспринмают такую запись как обращение к статическому элементу, потому, в этом случае, указывать слово typename не нужно. В случае, списка базовых классов однозначно известно, что имя является типом, а в случае инициализации элементов в списке конструктора однозначно известно, что имя есть вложенным элементом.

Источник: Скотт Мэйерс Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.

По мотивам правила 42.

Параметры по умолчанию в виртуальных функциях

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

Пример:

class Shape {
public:
enum ShapeColor { Red, Green, Blue };
// все фигуры должны предоставлять функцию для рисования
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle: public Shape {
public:
// заметьте, другое значение параметра по умолчанию – плохо!
virtual void draw(ShapeColor color = Green) const;
...
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const;
...
};

Shape *ps; // статический тип – Shape*
Shape *pc = new Circle; // статический тип – Shape*
Shape *pr = new Rectangle; // статический тип – Shape*

pr->draw(); // вызывается Rectangle::draw(Shape::Red)!

Поскольку статический тип pr – Shape*, то значения аргумента по умолчанию берутся из класса Shape, а не Rectangle.
Решение: невиртуальная функция базового класса + NVI.

Источник: Скотт Мэйерс Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.

По мотивам правила 37.

Чистая виртуальная функция

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

Пример 1:

struct TT
{
            virtual void foo () = 0;
};

void TT::foo()
{
            cout << "TT::foo" << endl;
}

struct T1 : TT
{
            virtual void foo ()
            {
TT::foo();                                                 //!
                        cout << "T1::foo" << endl;
            }
};

int main()
{
            TT* p = new T1;
            p->foo();
            p->TT::foo();                      // Требуется раскрытие области имен.
            delete p;
            return 0;
}

Пример 2:

class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
//...
};
void Airplane::fly(const Airport& destination) // реализация чисто
{ // виртуальной функции
//код по умолчанию, описывающий полет
//самолета в заданный пункт назначения
}

class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination);}
...
};
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination);}
//...
};
class ModelC: public Airplane {
public:
virtual void fly(const Airport& destination);
//...
};
void ModelC::fly(const Airport& destination)
{
//код, описывающий полет самолета ModelC в заданный пункт назначения
}

Источник: Скотт Мэйерс Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.

По мотивам правила 34.

Перегрузка функций в иерархиях классов

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

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

- перенаправляющие функции: определение новой функции, из которой вызывается функция базового класса.

Пример:

class Base
{
private:
            int x;
public:
            virtual void mf1() = 0;
            virtual void mf1(int);
            virtual void mf2();
            void mf3();
            void mf3(double);
            //...
};
class Derived: public Base
{
public:
            virtual void mf1();
            void mf3();
            void mf4();
            //...
};

Derived d;
int x;
//...
d.mf1(); // правильно, вызывается Derived::mf1
d.mf1(x); // ошибка! Derived::mf1 скрывает Base::mf1
d.mf2(); // правильно, вызывается Base::mf2
d.mf3(); // правильно, вызывается Derived::mf3
d.mf3(x); // ошибка! Derived::mf3 скрывает Base::mf3

Решение: используем using- включение имени базового класса.

class Base
{
private:
            int x;
public:
            virtual void mf1() = 0;
            virtual void mf1(int);
            virtual void mf2();
            void mf3();
            void mf3(double);
            //...
};
class Derived: public Base
{
public:
            using Base::mf1; // обеспечить видимость всех (открытых) имен
            using Base::mf3; // mf1 и mf3 из класса Base в классе Derived
            virtual void mf1()
            void mf3();
            void mf4();
            //...
};

Derived d;
int x;
//...
d.mf1(); // по-прежнему правильно, вызывается Derived::mf1
d.mf1(x); // теперь правильно, вызывается Base::mf1
d.mf2(); // по-прежнему правильно, вызывается Base::mf2
d.mf3(); // по-прежнему правильно, вызывается Derived::mf3
d.mf3(x); // теперь правильно, вызывается Base::mf3