company_banner

Спецификаторы, квалификаторы и шаблоны

    template<class T>
    static inline thread_local constexpr const volatile T x = {};

    Такое количество ключевых слов введет в ступор любого неподготовленного разработчика. Но на C++ Russia 2019 Piter Михаил Матросов (mmatrosov) разложил по полочкам квалификаторы и спецификаторы при объявлении переменных и функций.

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


    Из доклада вы узнаете:


    • как для переменных и функций сделать internal и external linkage;
    • почему inline для переменных обычно лучшем, чем extern;
    • особенности работы с шаблонами функций и переменных;
    • 8 способов объявить константу (ужас!);
    • какое светлое будущее обещает C++20.

    Кстати, перед выступлением наш журналист Олег Чирухин (olegchir) и Павел Филонов из программного комитета C++ Russia взяли у Михаила интервью, где он поделился интересными историями работы в Align Technology, а также опытом работы над онлайн-курсами.

    Далее — повествование от лица спикера.

    Немного теории


    Проведем небольшой теоретический экскурс, чтобы понять дальнейший материал доклада.

    Посмотрим, как происходит сборка программы на C++:



    В исходные cpp-файлы включают заголовочные hpp-файлы. Во время сборки первым начинает работу препроцессор. Из исходных файлов он формирует единицы трансляции (translation units), в которые собраны все заголовочные файлы (headers), а за ними идет тело cpp-файла. Конечно, компилятор по умолчанию не сохраняет их в явном виде на жестком диске, а они лежат в оперативной памяти.

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

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

    // Function declaration
    int sqr(int x);
    
    // Function definition
    int sqr(int x) { return x * x; }
    
    // Variable declarations
    extern int n;
    struct A { static int n; };
    
    // Variable definitions
    int n;
    int A::n;

    Перейдем к понятию linkage. Рассмотрим простенькую программу. В файле a.cpp содержится функция sqr():

    int sqr(int x) {
         return x * x;
    }

    А в файле b.cpp находится ее объявление и некоторая функция check():

    int sqr(int x);
    
    bool check(int a, int b, int c) {
        return sqr(a) + sqr(b) == sqr(c);
    }
    

    Программа скомпилируется, потому что определение функции в a.cpp имеет external linkage. Поэтому когда компилятор создаст объектные файлы, в a.obj он положит определение функции sqr(), а в b.obj — объявление функции с пометкой, что в каком-то файле лежит определение этой функции sqr(), и компоновщик его найдет. Если же в объявление функции мы добавим ключевое слово static, то программа не соберется из-за ошибки линковки. Так как функция sqr() будет иметь internal linkage, то есть будет недоступна в других единицах трансляции, и компоновщик её не найдёт.

    Кроме external linkage и internal linkage сущность может иметь статус no linkage. Така сущность доступна только в области видимости, в которой объявлена. Типичный пример — локальная переменная.

    Теперь вспомним типы storage duration в C++:

    • automatic — память для объекта выделяется в тот момент, когда поток выполнения заходит в scope, в котором переменная объявлена, и освобождается, когда поток выходит из scope;
    • static — память выделяется, когда программа начинает работу, и освобождается, когда программа завершает работу;
    • thread — похоже на static storage duration, но применимо к потоку выполнения;
    • dynamic — выделение памяти контролируется с помощью вызовов new и delete.

    Понятие storage duration применимо только к объектам, поскольку необходимо где-то в памяти хранить информацию. Все звучит достаточно просто, потому что задача сводится к выделению памяти. А вот момент, когда объект будет инициализирован, определить сложнее.

    Storage duration и linkage контролируются рядом ключевых слов (storage class specifiers) — static, extern, thread_local и mutable. Mutable не имеет отношения к Storage duration и linkage, и об этом в докладе больше не будет, но он формально является storage class specifier.

    На теоретическом экскурсе мы ответили на три вопроса:

    • Что? Объект.
    • Где? Linkage.
    • Когда? Storage duration.

    Однако C++ не был бы C++, если бы все было так просто.

    Internal и external linkage


    Рассмотрим пример. В некотором заголовочном файле common.hpp объявили две константы:

    const double thickness = 0.65;
    const char* name = "tooth";
    

    А в исходные файлы a.cpp и b.cpp включили этот hpp-файл:

    // a.cpp
    #include “common.hpp”
    
    // b.cpp
    #include “common.hpp”

    Это не скомпилируется, потому что есть несколько определений одного и того же имени name. Однако компилятор не ругается на thickness. Почему?

    Обратимся к C++ Reference:
    Any of the following names declared at namespace scope have internal linkage:

    • non-volatile non-template non-inline const-qualified variables (including constexpr) that aren't declared extern and aren't previously declared to have external linkage;


    Можно было бы подумать, что обе переменные const-qualified, поэтому имеют internal linkage, и их определения в единицах трансляции должны быть независимы. Однако name — это указатель, и ключевое слово const относится к объекту, на который он указывает. То есть он является указателем на константу, но не является константным указателем. Чтобы сделать его константным, нужно будет изменить запись:

    const char* const name = "tooth";

    Теперь name стал константным указателем на константу, получил internal linkage, и программа собирается без проблем.

    Давайте изменим пример:

    constexpr double thickness = 0.65;
    const std::string name = "tooth";
    

    Это скомпилируется, потому что name — константный символ, а спецификатор constexpr для объекта влечет за собой const, плюс linkage constexpr сущностей в явном виде описан в том же абзаце. Поэтому обе константы имеют internal linkage.
    Any of the following names declared at namespace scope have internal linkage:

    • non-volatile non-template non-inline const-qualified variables (including constexpr) that aren't declared extern and aren't previously declared to have external linkage;


    Перейдем к следующему примеру. В common.hpp оставим name и добавим функцию getName(), которая доступна из разных единиц трансляции:

    const std::string name = "tooth";
    const char* getName();

    В a.cpp мы сравниваем адреса буферов, которые возвращают name.data() и getName():

    #include "common.hpp"
    #include <iostream>
    
    bool dumbCmp(const char* s1, const char* s2) {
        return s1 == s2;
    }
    
    int main() {
        std::cout << std::boolalpha
            << dumbCmp(name.data(), getName());
    }

    В b.cpp мы определим функцию getName():

    #include "common.hpp"
    
    const char* getName() {
        return name.data();
    }

    Мы знаем, что name доступна в обеих единицах трансляции. Но одинаковая ли переменная в обоих случаях? Нет, программа напечатает false, потому что для каждой единицы трансляции создается отдельная копия name, а сравнение в dumbCmp() идет не по значению, а по адресу в памяти.

    Чтобы программа выдала true, добавим к определению name спецификатор inline:

    inline const std::string name = "tooth";

    В этом случае во всей программе будет только один объект name, и эта переменная получит особенный external linkage. В каждой единице трансляции все еще будет своя копия переменной на этапе компиляции, но когда этот символ попадет в объектный файл, то он получит пометку, что это weak символ. И компоновщик при объединении объектных файлов в программу выберет из нескольких одинаковых символов только один. В стандарте нет понятия external weak linkage, поэтому формально переменная будет иметь external linkage. Однако если попросить утилиты типа nm или dumpbin показать информацию об этой переменной в объектном файле, то они выведут именно external weak linkage.

    В другом примере в a.cpp и b.cpp включим заголовочный файл common.hpp, а в common.hpp запишем определение функции sqr():

    int sqr(int x) {
        return x * x;
    }

    Это не скомпилируется, потому что в каждой единице трансляции будет свое определение функции. Чтобы программа скомпилировалась, добавим спецификатор constexpr:

    constexpr int sqr(int x) {
         return x * x;
    }

    Если функция constexpr-qualified, то она считается inline. А спецификатор inline для функций также влечет external weak linkage. В современном C++ inline в первую очередь означает, что компоновщик выберет только один экземпляр данной сущности.

    Представим, что мы пишем какой-то main.cpp, где создаем класс Local и объявляем в нем функцию foo():

    // main.cpp
    void other();
    
    struct Local {
        static void foo() {
            std::cout << "main ";
        }
    };
    
    int main() {
        Local::foo();
        other();
    }

    Но другой разработчик в other.cpp тоже независимо завел класс Local и функцию foo():

    // other.cpp
    struct Local {
        static void foo() {
            std::cout << "other ";
        }
    };
    
    void other() {
        Local::foo();
    }

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

    main main

    GCC считает, что Local в разных файлах — это один и тот же класс, в нём есть функция foo(). Компилятор знает, что определения этой функции в разных файлах обязаны быть одинаковыми. Поэтому он взял первое попавшееся — из main.cpp. Другой компилятор мог бы вывести что-то другое.

    Эта проблема произошла из-за того, что класс Local имел external linkage. Чтобы исправить программу, положим классы в анонимное пространство имен (unnamed namespace):

    namespace {
        struct Local {
            static void foo() {
                std::cout << "main ";
            }
        };
    }

    Все сущности, которые оказываются в анонимном пространстве имен, всегда имеют internal linkage, то есть ничего из translation unit не может просочиться наружу. Поэтому программа будет работать так, как мы ожидаем:

    main other

    Собираем в кучу


    Посмотрим, какие существуют допустимые комбинации между storage duration и linkage:



    Для dynamic storage duration не имеет смысла концепция linkage, потому что мы выделяем объект в куче самостоятельно. Для automatic storage duration применимо только no linkage, ведь память под объект выделяется только при попадании в scope, то есть на этапе выполнения программы. Поэтому автоматические и динамические объекты мы не будем больше рассматривать, и говорить будем только о статических и thread_local объектах.

    Чтобы определить, какой storage duration у объекта, можно использовать блок-схему:



    Если сущность имеет спецификатор thread_local, то у нее thread storage duration. Если это не так, то нужно посмотреть на scope. Если переменная глобальная, то у нее всегда static storage duration. Для локальной переменной или члена класса проверяем наличие спецификатора static. Если он есть, то переменная статическая, иначе — автоматическая.

    Посмотрим, как эффекты, которые мы пронаблюдали, собираются вместе для разных видов сущностей:



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

    Для примера рассмотрим глобальную переменную. Из таблицы мы можем понять:

    • по умолчанию она имеет external linkage;
    • если она объявлена constexpr, то она также будет const;
    • если она обозначена как const, то спецификатор влечет за собой internal linkage (но только если нет спецификаторов volatile и template);
    • если она inline, то она имеет external (weak) linkage;
    • если она static, то она имеет internal linkage, игнорируя предыдущие пункты;
    • если она лежит в анонимном пространстве имен, то она всегда имеет internal linkage.

    Запись N/A в таблице означает, что ключевое слово из соответствующего свойства для данной сущности неприменимо. Например, inline неприменим к локальной переменной.

    А под записью Required подразумевается, что эти сущности обязаны иметь ключевое слово из соответствующего свойства, чтобы вообще попасть в эту таблицу. Например, если у поля класса не будет спецификатора static, то оно вообще не попадёт в эту таблицу.

    Спецификатор extern


    В примере, где мы сравнивали буферы, мы использовали inline, чтобы программа вывела true. Однако это не единственный способ решения задачи.

    До C++17 не было inline-переменных, и мы могли объявить переменную name как extern:

    extern const std::string name;

    Тогда бы переменная получила external linkage и превратилась в объявление (declaration). Но в этом случае необходимо где-то добавить явное определение для переменной name, и мы вставляем его в a.cpp:

    const std::string name = "tooth";

    Таким образом мы бы получили тот же результат выполнения программы.

    Какими свойствами обладает extern?

    • Применим только к глобальным функциям и переменным.
    • Несовместим со static.
    • Не имеет смысла с constexpr и с inline.
    • Значение не видно в точке объявления (обычно недостаток).

    У extern есть недостаток: необходимо вручную делать определение, то есть явно выбрать единицу трансляции, в которой переменная будет определена. Однако теоретически extern позволяет оптимизировать время сборки. Поскольку мы делаем определение вручную, то extern дает возможность избежать дополнительной нагрузки на компоновщик, поскольку символы не будут попадать в каждый объектный файл, как это происходит с inline.

    Но это довольно специфический момент, и обычно вместо extern лучше использовать inline.

    Добавим extern в таблицу комбинаций свойств и сущностей:



    Для не глобальных сущностей extern неприменим. Для глобальных же функций данный спецификатор излишен, потому что любое объявление глобальной функции по умолчанию является extern. Но для глобальных переменных спецификатор будет работать, и для переменной он будет указывать external linkage и превращать ее в объявление переменной.

    Practice time


    От теории перейдем к практике. Рассмотрим такой класс:

    struct A
    {
        double x1;
        static double x2;
        static const double x3;
        static inline const double x4 = 4.0;
        static constexpr double x5 = 5.0;
    };

    Посмотрим на таблицу. Нас интересует колонка member variable. Какие выводы мы можем сделать?

    • x1 имеет automatic storage duration и не может иметь linkage;
    • x2, x3, x4 и x5 имеют static storage duration;
    • x2 и x3 имеют external linkage. Причем x2 и x3 являются объявлениями.
    • x4 и x5 имеют external (weak) linkage, поскольку они inline (constexpr влечет за собой inline для членов класса). Мы можем указать инициализацию прямо в теле класса. И компоновщик позаботится о том, чтобы определения не конфликтовали в разных единицах трансляции.

    А что такое static constexpr? Мы знаем, что переменная с constexpr используется только на этапе компиляции, а static это про storage duration, который имеет смысл только на этапе выполнения. Может, вообще нет никакого storage duration, если она доступна только во время компиляции?

    Не совсем. constexpr и static находятся в разных мирах. constexpr действителен только при компиляции, и после этого процесса от constexpr не остается и следа (ну, точнее, от него останется const или inline, в соответствии с таблицей свойств). Но когда программа начинает выполняться, те же самые переменные, которые использовались на этапе компиляции, начинают существовать уже на этапе выполнения. К ним становится применим спецификатор static, потому что только на стадии выполнения у них есть storage duration.

    Стоит вспомнить еще одну «парочку» ключевых слов — const и volatile. const означает, что мы не можем из программы менять наш объект. volatile разрешает менять и читать объект кому-то другому извне программы. const volatile переменную мы менять не можем, но ее может изменить кто-то другой. Кроме того, в практически любом контексте, где используется const, можно применить volatile.

    Шаблоны


    Функции, классы и переменные могут быть шаблонами. Однако важно понимать, что не бывает шаблонных сущностей (template entity), а есть только шаблоны сущностей (entity template). Сравним функцию и шаблон:

    • шаблон нельзя вызвать, как функцию;
    • у шаблона нельзя взять адрес, в отличие от функции;
    • шаблон нельзя перегрузить.

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

    У шаблона есть неявные инстанциации. Но компоновщик сам позаботится о них в разных модулях трансляции. Их linkage не так важен и даже не всегда понятен.

    Перейдем к примеру. Заведем три шаблона переменных в заголовочном файле:

    template<class T> bool b = true;
    template<class T> const bool cb = true;
    template<class T> inline const bool icb = true;

    Включаем hpp-файл в два cpp-файла. Далее инстанцируем переменные: b, cb и icb. В каждой единице трансляции мы берем адрес у этих инстанциаций и выводим. Компилятор clang выдал:

    0x6030c0 0x401ae4 0x401ae5 // first translation unit
    0x6030c0 0x401ae4 0x401ae5 // second translation unit

    Мы видим одни и те же адреса. Значит, программа работала с одними и теми же объектами. Скомпилируем программу с помощью gcc и посмотрим результат:

    0x6015b0 0x400ef5 0x400ef4 // first translation unit
    0x6015b0 0x400ef6 0x400ef4 // second translation unit

    Для const bool cb внезапно различаются адреса. Я даже задал вопрос на stackoverflow и получил интересный ответ:



    Стандарт не очень явно объясняет, какой будет linkage у инстанциации шаблонов. Поэтому мы тоже не будем настолько углубляться в эти детали. Если вы хотите убедиться, что используется один и тот же объект, то используйте inline, который не подведет. Например, стандартная константа std::is_const_v, как и другие стандартные константы, объявляется так:

    template<class T>
    inline constexpr bool is_const_v = is_const<T>::value;

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

    Как уже говорилось ранее, у шаблонов в большинстве случаев неявная инстанциация, достаточно поставить угловые скобки. Есть не очень известный, но полезный механизм — объявление явной инстанциации (explicit instantiation declaration).

    Пусть в header.hpp есть некоторый шаблон большой сложной функции:

    template<class T>
    int complicatedTemplateFunction(const T& x) {
        // Some complicated stuff
    }

    Мы можем написать extern template и указать сущность с конкретным типом:

    extern template int complicatedTemplateFunction(const std::string& x);

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

    Поскольку у нас есть объявление явной инстанциации, куда-то нужно будет поместить её определение:

    template int complicatedTemplateFunction(const std::string& x);

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

    Долгий путь к const


    Константы до C++17 могли быть объявлены в заголовочном файле кучей разных способов:

    #define n 42

    Тут вроде бы уже все знают, что так делать не стоит.

    const int n = 42;

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

    extern const int n;

    Неплохо, но значение, которое мы инициализируем, пропадает, и нужно искать в программе его определение, что неудобно.

    inline int n() {
        return 42;
    }

    Так тоже можно, но не получится взять адрес, потому что будет возвращаться rvalue. Да и ещё скобки нужно будет писать при использовании.

    enum {
         n = 42
    };

    Весьма неплохой подход, но работает только для целочисленных типов.

    Начиная с C++17 мы можем использовать inline, который будет работать для любого типа. В заголовочном файле это будет выглядеть так:

    inline constexpr int n1 = 1; // Default choice
    inline const std::string s2 = "2"; // If not a literal type

    На этапе компиляции второй вариант использовать не получится, но в остальном будет все то же самое, что и при constexpr.

    Если мы объявляем константу в cpp-файле, то она должна быть доступна только в текущей единице трансляции:

    constexpr int n3 = 3; // Default choice; implicitly static
    const std::string s4 = "4"; // If not a literal type; implicitly static

    Убираем inline, иначе объявление константы может интерферировать с другой единицей трансляции. Кстати, в module interface unit в C++20 можно использовать тот же синтаксис.

    Если константа — член класса, то она объявляется как static:

    struct A {
        static constexpr int n = 5; // Default choice; implicitly inline
        static inline const std::string s = "6"; // If not a literal type
    };

    Если к константе нельзя применить constexpr, то придется вручную прописать inline, потому что для поля класса его компилятор не подставит, в отличие от функций.

    Если же константа — локальная переменная, то синтаксис похож на объявление глобальной переменной, но со static:

    void f() {
        static constexpr int n = 7; // Default choice
        static const std::string s = "8"; // If not a literal type
    }

    Целых 8 вариантов. Но все не так сложно, как кажется. Асимметрия между constexpr и const наблюдается только в случае, когда константа — член класса.

    Когда в светлом будущем, допустим, останутся только модули и не будет заголовочных файлов, останутся только эти варианты:

    // module.ixx
    constexpr int n3 = 3;
    
    // Anywhere
    struct A {
        static constexpr int n = 5;
    };
    
    void f() {
        static constexpr int n = 7;
    }

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



    Она описывает ровно те примеры, что мы разобрали выше.

    Загадочный пример из описания


    Рассмотрим пример, который был в описании доклада:

    template<class T>
    static inline thread_local constexpr const volatile T x = {};


    Попробуем его оптимизировать:

    1. const не нужен, потому что уже есть constexpr, поэтому убираем.
    2. Мы знаем по таблице, что static перебивает inline, поэтому можем смело убирать inline.

    В итоге у нас остается:

    template<class T>
    static thread_local constexpr volatile T x = {};

    static для глобальной переменной даёт internal linkage. thread_local говорит о том, что будет thread storage duration. Поэтому x — это constexpr volatile шаблон переменной с thread storage duration и internal linkage (constexpr volatile variable template with thread storage duration and internal linkage).

    Изменения в C++20




    В C++ 20 добавляется еще один вид linkage — module linkage. external linkage становится module linkage, потому что это linkage внутри модуля, а все, что выходит за пределы модуля, становится external linkage.

    В C++20 появляется спецификатор для функции consteval. Это как constexpr, но если constexpr функция может работать как на этапе компиляции, так и на этапе выполнения, то consteval доступен только на этапе компиляции.

    Для удобства можно считать, что consteval функция недоступна на этапе компоновки и выполнения, не генерирует символа в объектном файле и является своеобразным функциональным макросом. На самом деле в стандарте вообще нет таких понятий, как “время компиляции” и “время выполнения”. Есть только “наблюдаемый эффект выполнения программы”. Однако формулировка consteval дана таким образом, чтобы реальные компиляторы имели возможность реализовать ожидаемое поведение.

    Для переменных в C++ добавили спецификатор constinit. Если constinit переменную попытаться инициализировать чем-то, что неизвестно на этапе компиляции, то компилятор выдаст ошибку. Забавно, что constinit не означает, что переменная является const. Он значит только то, что переменная должна быть инициализирована в момент компиляции, а во время выполнения ее можно изменять.

    Добавим consteval и constinit в таблицу:



    Как жить с особенностями C++ и не сойти с ума


    • Помещайте всё в анонимное пространство имен, если это возможно. Подумайте, сможете ли вы полностью отказаться от static для глобальных переменных в пользу анонимного пространства имен.
    • Предпочитайте inline вместо extern.
    • Предпочитайте constexpr вместо const.
    • Старайтесь использовать переменные со static и thread storage duration только для констант. Иначе изменчивое глобальное состояние будет влиять на надёжность, дизайн и тестируемость.

    В этом году на конференции С++ Russia 2020 Moscow выступят сам создатель языка С++ Бьярне Страуструп и председатель комитета по стандартизации С++ Герб Саттер! Еще больше знаменитых спикеров можно будет увидеть по билету-абонементу, который дает доступ ко всем 8 конференциям летнего сезона.
    JUG Ru Group
    Конференции для программистов и сочувствующих. 18+

    Комментарии 9

      0
      Все сущности, которые оказываются в анонимном пространстве имен, всегда имеют internal linkage


      Эмм, я возможно ошибаюсь, но когда-то я уже с этим разбирался, и, нет, у них не internal linkage в чистом виде. Просто имя сущности оказывается при трансляции гарантированно уникальным и для него не находится соответствий при линковке. В этом смысле анонимные пространства имен не являются полным аналогом static-сущностей единицы трансляции (хотя в интернете часто именно так и пишут), но результат в общем оказывается один и тот же.
        0
        Так было до C++11, начиная с C++11 они должны иметь internal linkage:

        en.cppreference.com/w/cpp/language/storage_duration

        Но… тут не без сюрпризов. Вот здесь:

        godbolt.org/z/yL2WUT

        и clang и gcc линкуют к extern i только i, объявленную как static, если строчку с ее объявлением закомментировать, получим ошибку линковки. В то же время MSVC ведет себя «более по стандарту» — сразу ругается на ambiguous symbol i, и если закомментировать строку со static, то он линкует extern i к i из анонимного namespace, как и положено. Думаю, это баг, но поскольку такого рода вещи в реальном коде используются редко, то его никто особо не чинит :)
          0
          Так было до C++11, начиная с C++11 они должны иметь internal linkage

          Пожалуй вы правы, наверное только так можно понять эту вставку здесь:
          Even though names in an unnamed namespace may be declared with external linkage, they are never accessible from other translation units because their namespace name is unique. (until C++11)
          Unnamed namespaces as well as all namespaces declared directly or indirectly within an unnamed namespace have internal linkage, which means that any name that is declared within an unnamed namespace has internal linkage. (since C++11)

          Хотя все же точный механизм неочевиден.

          Но… тут не без сюрпризов. Вот здесь:
          godbolt.org/z/yL2WUT
          и clang и gcc линкуют к extern i только i, объявленную как static, если строчку с ее объявлением закомментировать, получим ошибку линковки

          Эм… Весь код расположен в одном модуле, стало быть оба определения модуле-локальных переменных i во-первых не должны транслироваться наружу (и тогда объявление extern int i вообще непонятно какой смысл имеет, причем его можно безболезненно удалить), а во-вторых они ambiguous по определению, компилятор обязан ругаться в соответствии со стандартом (все по той же ссылке выше). Это что за хрень такая?
            0

            extern int i в данном случае нужен всего-навсего для проверки linkage. Если internal linkage работает, то extern int i должен линковаться как к static int i, так и к i из анонимного namespace в пределах того же модуля. В MSVC это так и есть, а в gcc и clang internal linkage работает только со static, что стандарту не очень-то соответствует.

              0
              Я примерно так и понял, просто пример вышел очень контринтуитивный — обычно «extern» используется для указания того, что данная переменная определена (и память под нее выделена) в каком-то другом модуле, а тут мы пытаемся связаться с переменными в этом же модуле (при том, что они и так видны, будучи глобальными в рамках модуля). При этом, повторюсь, странно, что компилятор вообще не задает вопросов о том, к какой именно переменной мы обращаем.
                0
                Ну extern необязательно означает «в другом модуле», скорее «где-то», и это «где-то» вполне может быть и в текущем модуле. MSVC вот как раз и задает этот вопрос, для него неочевидно, к какой именно i этот extern привязать, а gcc и clang — нет, они привязывают ее только к static. Однако, если убрать extern, то и у них тоже появляются вопросы насчет ambigious.
                  +1
                  Ну extern необязательно означает «в другом модуле», скорее «где-то», и это «где-то» вполне может быть и в текущем модуле


                  Ну вот это как раз и контринтуитивно для меня. Ни разу не использовал extern для обращения к переменным внутри того же модуля )))

                  gcc и clang — нет, они привязывают ее только к static. Однако, если убрать extern, то и у них тоже появляются вопросы насчет ambigious.


                  И это поведение в самом деле очень странное… Просто убиться веником.
        0

        Опечатка:
        // other.cpp
        struct Local {
        static void foo() {
        std::cout << "main "; // надо other
        }
        };

          0
          Спасибо за внимательность, поправили :)

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое