Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Программирование и алгоритмические языки


Ulrich Drepper "Как писать разделяемые библиотеки"
Назад Оглавление Вперед

2.2.4. Определение видимости для классов C++

Для кода на C++ мы можем также использовать атрибуты, но их следует использовать с большой осторожностью. Обычные определения функций или переменных могут обрабатываться точно также, как в языке C. Добавочные трансформации имен, возникающие при их обработке, не должны влиять на их видимость. Другая история, когда речь идет о классах. Символы и код, созданные для определения классов, являются функциями - членами класса и статическими данными класса или статическими функциями — членами класса. Эти переменные и функции можно легко объявить как hidden (скрытые), но нужно быть осторожным. Рассмотрим первый пример следующей синтаксической структуры:

class foo {
   static int u __attribute__
	((visibility ("hidden")));
   int a;
 public:
   foo (int b = 1);
   void offset (int n);
   int val () const __attribute__
     ((visibility ("hidden")));
};
int foo::u __attribute__
   ((visibility ("hidden")));
foo::foo (int b) : a (b) { }
void foo::offset (int n) { u = n; }
int
__attribute__ ((visibility ("hidden")))
foo::val () const { return a + u; }

В этом примере кода статический член данных класса и функция-член класса val определены как hidden (скрытые). Символы недоступны вне объекта DSO, в котором указаны определения. Пожалуйста, обратите внимание, что это дополнительное ограничение, действующее поверх правил доступа для C++. Один из способов решения проблемы для функций-членов класса является создание экземпляра класса в более чем одном DSO. Как правило, это не вызывает каких-либо проблем и "лишь" ведет к увеличению размера кода.

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

В примере кода, приведенном выше, данные члена класса u, имеющего тип static, объявлены как hidden (скрытые). Все те, сущности их используют, должны быть определены в том же самом объекте DSO. Правила доступа C++ ограничивают доступ только к функциям-членам класса, независимо от того, где эти функции определены. Чтобы быть уверенным, что все сущности, которые их используют, определены в том же самом объекте DSO, в котором определено u, нужно, как правило, избегать использовать встроенные функции inline, которые имеют доступ к скрытым данным, поскольку код, сгенерированный для встроенных функций inline, может быть размещен в любом объекте DSO, в котором содержится код, использующий определение класса. Ярким примером такой функции является функция-член класса offset, которая должна быть функцией inline, но поскольку она обращается к u, этого делать нельзя. Вместо этого offset экспортируется как интерфейс из DSO, в котором находится определение u.

Если функция-член класса, такая, как функция val, которая есть в примере, помечена как hidden (скрытая), то ее нельзя вызывать извне объекта DSO. Обратите внимание, что в примере, компилятор разрешает глобальный доступ к функции-члену класса, поскольку эта функция определена как public. Если эта функция-член класса будет использоваться вне объекта DSO, то жалоба поступит от компоновщика, а не от компилятора. Неопытные или не полностью информированные пользователи могут интерпретировать эту проблему, как отсутствие экземпляра класса, что затем приводит к проблемам из-за наличия нескольких определений.

Т.к. эти проблемы так трудно отлаживать, важно, чтобы в проверке того, что пользователи соблюдают необходимые правила, участвовал компилятор. Система типов C++ достаточно развита с тем, чтобы помочь в случае, если разработчик для этого приложит некоторые дополнительные усилия. Ключом является как можно более точная имитация реального доступа к символу, использующая определение класса. По этой причине определение классов в приведенном выше примере в действительности должны выглядеть следующим образом:

class foo {
   static int u __attribute__
	((visibility ("hidden")));
   int a;
 public:
   foo (int b = 1);
   int val () const __attribute__
	((visibility ("hidden")));
   void offset (int n);
};

class foo_ext : protected foo {
 public:
   foo_ext (int b = 1) : foo (b) { }
   void offset (int n)
	{ return foo::offset (n); }
};

Класс foo рассматривается как приватный класс private, который не должен использоваться с экземплярами, находящимися вне объекта DSO. Публичным интерфейсом public мог бы быть класс foo ext. Он обеспечивает доступ к двум публичным интерфейсам классов, лежащих глубже. До тех пор, пока пользователи объекта DSO, содержащего определения, соблюдают требование, что может использоваться только класс foo ext, у компилятора нет возможности вне объекта DSO, содержащего определения. узнать о способе доступа к foo::u и foo::val.

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

template<class T>
class a {
   T u;
 public:
   a (T a = 0);
   T r () const __attribute__
	((visibility ("hidden")));
};

template<class T> a<T>::a (T a)
 { u = a; }
template<class T> T
__attribute__ ((visibility ("hidden")))
   a<T>::r () const { return u; }

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

Одним из видов функций, которые можно безопасно хранить локально и не экспортировать, являются встроенные функции вида inline, которые можно определить либо в определении класса или отдельно. В каждом откомпилированном блоке должен быть свой собственный набор всех используемых функций inline. И было бы лучше, чтобы все функции из всех объектов DSO и исполняемых модулей были бы одинаковыми и, следовательно, взаимозаменяемыми. Можно явно пометить все функции inline как скрытые, но это большая работа. Начиная с версии 4.0 в компиляторе gcc можно использовать параметр -fvisibility-inlines-hidden, который делает именно то, что следует из его названия (скрывает видимость функций inline). Если используется этот параметр, то предполагается, что рассматриваемая функция inline будет скрытой, а копии этой функции будут помечены как STV HIDDEN. То есть, если функция не является функцией типа inline, то создаваемая отдельная функция не экспортируется. Это довольно частая ситуация, случающаяся время от времени, поскольку не все функции, которые по мнению программиста должны быть типа inline, должны анализироваться компилятором. Этот параметр можно использовать почти во всех ситуациях. Этот параметр можно не указывать только в том случае, если функции, находящиеся в различных объектах DSO, различаются или если код будет всегда зависеть точно от одной копии используемой функции (например, если предполагается, что адрес функции будет всегда одним и тем же).

Если класс С++ используется только для реализации и не используется в коде в каком либо интерфейсе объекта DSO, использующего код, то можно было бы пометить каждую функцию-член класса и данные класса типа static как hidden (скрытые). Это трудоемко, можно внести ошибки и не всегда это можно сделать полностью. Возможно, есть большое количество членов класса, которые нужно пометить, и, когда добавляется новый член класса, легко забыть о добавлении необходимых атрибутов. То, что все члены класса пометить не удастся, связана с тем, что компилятор C++ автоматически генерирует несколько функций-членов класса, такие как конструкторы и деструкторы. Действие атрибутов на эти функции-члены класса распространяться не будет.

Решение этих проблем состоит в явном определении видимости всего класса. Для этого в компиляторе gcc, начиная с версии 4.0, имеется поддержка. Есть два способа достичь этой цели. Во-первых, может быть использована уже упомянутая директива pragma.

#pragma GCC visibility push(hidden)
class foo {
...
};
#pragma GCC visibility pop

Все функции-члены класса и данные класса типа static автоматически определяются в foo как скрытые hidden. Если необходимо, то это даже расширяется на неявно порождаемые функции и операторы.

Вторая возможность заключается в использовании еще одного расширения, имеющегося в gcc 4.0. Оно позволяет помечать функцию как hidden (скрытая), когда она определяется. Синтаксис следующий:

class __attribute ((visibility ("hidden")))
foo {
...
};

Точно также как и в случае с pragma, все определяемые функции определяются как символы hidden. Предпочтительнее использовать атрибуты явно, поскольку эффект от директив pragma не всегда очевиден. Если добавляемые и удаляемые строчки кода достаточно далеко разнесены друг от друга, программист может случайно добавить между ними новое объявление, даже если это не будет следовать, что на видимость этого нового объявления будет оказано воздействие. Оба варианта, pragma и атрибут класса, должны использоваться только во внутренних заголовках. В заголовках, которые используются для предоставления интерфейсов API в объекте DSO, так делать не имеет никакого смысла, поскольку задача в том, чтобы скрыть детали реализации. Это значит, что всегда хорошо проводить различие между внутренними и внешними файлами заголовков.

При определении целых классов с видимостью hidden есть некоторые проблемы, которые нельзя смоделировать с помощью сложно организованных классов или с помощью перемещения определения в приватные заголовки. Для обработки исключительных состояний компилятор генерирует структуры данных (символы typeinfo), которые также помечаются в соответствие с используемым атрибутом видимости. Если объект данного типа вырабатывает исключительное состояние, то операция catch ищет информацию typeinfo. Если эта информация находится в другом объекте DSO, то поиск будет неудачным и программа завершит свою работу. Не гарантируется, что все классы, которые используются при обработке исключительных состояний и в которых где вырабатываются и отлавливаются исключительные состояния, будут находиться в объекте DSO с определением, которое должно быть объявлено с видимостью, используемой по умолчанию. Отдельные члены класа все еще может быть помечены атрибутом видимости, но поскольку данные typeinfo синтезируются компилятором, у программистов нет никакой возможности переопределить атрибут видимости hidden, ассоциированный с конкретным классом.

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


Предыдущий раздел:   Следующий раздел:
Назад Оглавление Вперед