Почему стоит предпочитать неназначенные пространства имен функциям с static?
У C++ есть возможность создавать анонимные (безымянные) пространства имен, вот так:
namespace {
int cannotAccessOutsideThisFile() { ... }
} // namespace
На первый взгляд может показаться, что такая функция бесполезна — поскольку вы не можете задать имя пространства имен, невозможно получить доступ к его содержимому извне. Тем не менее, эти безымянные пространства имен доступны в пределах файла, где они созданы, как если бы у вас была неявная директива using
.
У меня вопрос: в каких случаях использование анонимных пространств имен предпочтительнее по сравнению с использованием статических функций? Или это по сути два способа достижения одной и той же цели?
5 ответ(ов)
Есть один крайний случай, где использование ключевого слова static
приводит к неожиданным последствиям (по крайней мере, это было неожиданно для меня). Стандарт C++03 в разделе 14.6.4.2/1 гласит:
Для вызова функции, зависящего от параметра шаблона, если имя функции является unqualified-id, но не template-id, кандидатные функции ищутся с использованием обычных правил поиска (3.4.1, 3.4.2), за исключением того, что:
- Для части поиска, использующей unqualified name lookup (3.4.1), находятся только объявления функций с внешней связью из контекста определения шаблона.
- Для части поиска, использующей связные пространства имен (3.4.2), находятся только объявления функций с внешней связью, найденные либо в контексте определения шаблона, либо в контексте инстанциации шаблона.
...
Ниже приведенный код вызовет foo(void*)
, а не foo(S const &)
, как можно было бы ожидать:
template <typename T>
int b1 (T const & t)
{
foo(t);
}
namespace NS
{
namespace
{
struct S
{
public:
operator void * () const;
};
void foo (void*);
static void foo (S const &); // Не учитывается 14.6.4.2(b1)
}
}
void b2()
{
NS::S s;
b1 (s);
}
Сам по себе этот момент, вероятно, не так уж важен, но он подчеркивает, что для полностью соответствующего стандарту C++ компилятора (то есть поддерживающего export
) ключевое слово static
все равно будет иметь функционал, который недоступен другим способом.
// bar.h
export template <typename T>
int b1 (T const & t);
// bar.cc
#include "bar.h"
template <typename T>
int b1 (T const & t)
{
foo(t);
}
// foo.cc
#include "bar.h"
namespace NS
{
namespace
{
struct S
{
};
void foo (S const & s); // Будет найден другим TU 'bar.cc'
}
}
void b2()
{
NS::S s;
b1 (s);
}
Единственный способ гарантировать, что функция из нашего анонимного пространства имен не будет найдена в шаблонах, использующих ADL, — это сделать ее static
.
Обновление для современного C++
Начиная с C++11, члены анонимного пространства имен имеют внутреннюю связь по умолчанию (3.5/4):
Неименованное пространство имен или пространство имен, объявленное непосредственно или опосредованно внутри неименованного пространства имен, имеет внутреннюю связь.
Однако, в то же время, 14.6.4.2/1 было обновлено так, чтобы убрать упоминание о связи (это взято из C++14):
Для вызова функции, где постфикс-выражение является зависимым именем, кандидатные функции находятся с использованием обычных правил поиска (3.4.1, 3.4.2), за исключением того, что:
- Для части поиска, использующей unqualified name lookup (3.4.1), находятся только объявления функций из контекста определения шаблона.
- Для части поиска, использующей связные пространства имен (3.4.2), находятся только объявления функций, найденные либо в контексте определения шаблона, либо в контексте инстанциации шаблона.
В результате этого конкретное различие между static
и членами анонимного пространства имен больше не существует.
Недавно я начал заменять статические ключевые слова на анонимные пространства имен в своем коде, но сразу столкнулся с проблемой: переменные в пространстве имен больше не были доступны для просмотра в отладчике. Я использовал VC60, поэтому не знаю, является ли это несущественной проблемой в других отладчиках. Мое решение заключалось в определении 'модульного' пространства имен, которое получало имя моего cpp файла.
Например, в моем файле XmlUtil.cpp я определяю пространство имен XmlUtil_I { ... }
для всех моих модульных переменных и функций. Таким образом, я могу использовать квалификацию XmlUtil_I::
в отладчике, чтобы получить доступ к переменным. В этом случае _I
отличает его от публичного пространства имен, такого как XmlUtil
, которое я хотел бы использовать в других местах.
Я предполагаю, что потенциальным недостатком этого подхода по сравнению с истинно анонимным является то, что кто-то может нарушить желаемую статическую область, используя квалификатор пространства имен в других модулях. Однако не знаю, является ли это серьезной проблемой.
Разница заключается в именах сжатых идентификаторов (_ZN12_GLOBAL__N_11bE
против _ZL1b
), что, в общем-то, не имеет большого значения, но оба они собраны как локальные символы в таблице символов (из-за отсутствия директивы .global
в ассемблере).
#include<iostream>
namespace {
int a = 3;
}
static int b = 4;
int c = 5;
int main (){
std::cout << a << b << c;
}
На уровне ассемблера это выглядит так:
.data
.align 4
.type _ZN12_GLOBAL__N_11aE, @object
.size _ZN12_GLOBAL__N_11aE, 4
_ZN12_GLOBAL__N_11aE:
.long 3
.align 4
.type _ZL1b, @object
.size _ZL1b, 4
_ZL1b:
.long 4
.globl c
.align 4
.type c, @object
.size c, 4
c:
.long 5
.text
Что касается вложенного анонимного пространства имен:
namespace {
namespace {
int a = 3;
}
}
Ассемблерный код будет следующим:
.data
.align 4
.type _ZN12_GLOBAL__N_112_GLOBAL__N_11aE, @object
.size _ZN12_GLOBAL__N_112_GLOBAL__N_11aE, 4
_ZN12_GLOBAL__N_112_GLOBAL__N_11aE:
.long 3
Все анонимные пространства имен первого уровня в единице трансляции объединяются друг с другом, а все вложенные анонимные пространства имен второго уровня также объединяются между собой.
Вы также можете иметь вложенное пространство имен или вложенное встроенное пространство имен в анонимном пространстве имен:
namespace {
namespace A {
int a = 3;
}
}
Ассемблерный код получится таким:
.data
.align 4
.type _ZN12_GLOBAL__N_11A1aE, @object
.size _ZN12_GLOBAL__N_11A1aE, 4
_ZN12_GLOBAL__N_11A1aE:
.long 3
Для справки, это можно декодировать как:
.data
.align 4
.type (анонимное пространство имен)::A::a, @object
.size (анонимное пространство имен)::A::a, 4
(анонимное пространство имен)::A::a:
.long 3
Вложенные анонимные пространства имен также могут быть встроенными, но, насколько я могу судить, inline
в тогда как для анонимных пространств имен не оказывает никакого влияния.
inline namespace {
inline namespace {
int a = 3;
}
}
Теперь что касается сжатия идентификаторов. В _ZL1b
_Z
означает, что это сжатый идентификатор. L
указывает, что это локальный символ (через static
). 1
— это длина идентификатора b
, а затем сам идентификатор b
.
В _ZN12_GLOBAL__N_11aE
_Z
также указывает на сжатый идентификатор. N
показывает, что это пространство имен, 12
— длина имени анонимного пространства имен _GLOBAL__N_1
, далее идет само имя анонимного пространства имен, затем 1
— длина идентификатора a
, идентификатор a
, и E
завершает идентификатор, находящийся в пространстве имен.
В _ZN12_GLOBAL__N_11A1aE
ситуация аналогична, за исключением того, что в представленной структуре имеется еще одно пространство имен (1A
), которому предшествует длина A
, которая равна 1. Все анонимные пространства имен имеют имя _GLOBAL__N_1
.
Использование ключевого слова static
для этой цели устарело согласно стандарту C++98. Проблема с static
заключается в том, что оно не применяется к определению типа. Кроме того, это перегруженное ключевое слово, которое используется различными способами в разных контекстах, поэтому анонимные пространства имен упрощают ситуацию.
Согласно моему опыту, отмечу, что хотя в C++ принято помещать ранее статические функции в анонимное пространство имен, на старых компиляторах это иногда вызывает проблемы. В настоящее время я работаю с несколькими компиляторами для наших целевых платформ, и более современный компилятор для Linux вполне нормально обрабатывает функции в анонимном пространстве имен.
Однако старый компилятор, работающий на Solaris, который мы вынуждены использовать до неопределенного момента, иногда принимает это, а иногда выдает ошибку. Меня больше беспокоит не ошибка как таковая, а то, что может происходить, когда компилятор принимает это. Поэтому, пока мы не перейдем на современные технологии повсеместно, мы по-прежнему используем статические функции (обычно с областью видимости класса) там, где предпочли бы использовать анонимное пространство имен.
В чем проблема с "using namespace std;"?
Что такое Правило трёх?
Каков эквивалент статических методов Java в Kotlin?
Имеют ли круглые скобки после имени типа значение при использовании new?
Как удалить элемент из std::vector<> по индексу?