среда, 24 июля 2013 г.

C++: члены класса в тени формальных параметров конструктора

Возьмем простую программу
class  A
{
    public:
        explicit A( int  a ) : a( a )
        {}

    private:
        int  a;
};

int  main( void )
{
    A  a( 0 );

    return 0;
}
Что здесь плохо? Видите ли вы потенциальную опасность? Попробуем скомпилировать с включенными предупреждениями:
g++ -Wall -o test test.cc
Всё хорошо. Запуск программы на выполнение также не приводит к каким-либо проблемам (и это неудивительно). А теперь попробуем так:
g++ -Wall -Wshadow -o test test.cc
test.cc: In constructor «A::A(int)»:
test.cc:23:30: предупреждение: декларация «a» перекрывает элемент класса, на который указывает 'this' [-Wshadow]
         explicit A( int  a ) : a( a )
                              ^
Итак, с помощью опции компилятора -Wshadow нам удалось получить интересное предупреждение, которое намекает на то, что если мы будем использовать имя a внутри тела конструктора, то это никоим образом не будет член класса A, но одноименный формальный параметр, переданный в конструктор. Вот так то! Всё бы ничего, мы ведь сами не дураки и знаем, что если нам понадобится член класса, то мы сможем обратиться к нему через указатель this.

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

Как же с этим бороться? Самый очевидный способ - рефакторинг. Имена формальных параметров конструкторов и членов класса должны отличаться. Например, именем формального параметра в приведенном примере остается a, а имя члена класса переименовываем в a_. Лично мне такой стиль совершенно не импонирует. В самом деле, инициализацию членов класса через формальные параметры конструктора хотелось бы рассматривать как чисто языковой элемент, не вдаваясь в подробности реализации, такие как копирование одного объекта в другой. Иначе страдает абстракция и такой, казалось бы, простой шаблон, как инициализация членов класса формальными параметрами конструктора, начинает обрастать ненужными деталями.

Второй способ - обрамление объявлений конструкторов прагмами компилятора, отключающими диагностические сообщения. Например, в случае gcc конструктор A из приведенного выше примера может быть записан так:
#pragma GCC diagnostic ignored "-Wshadow"
        explicit A( int  a ) : a( a )
        {}
#pragma GCC diagnostic pop
Минусы здесь очевидны. Во-первых, мы так и не добились желаемого уровня абстракции шаблона инициализации членов класса, засорив его еще менее понятными объявлениями. Во-вторых, прагмы, как известно, не переносимы, и для разных компиляторов они будут отличаться. В-третьих, если проект достаточно большой, то вставка большого количества прагм замусорит код.

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