[clang] [Clang] Diagnose UB and emit error when identifier has both internal and external linkage (PR #192116)

Vlad Serebrennikov via cfe-commits cfe-commits at lists.llvm.org
Wed Apr 22 01:24:57 PDT 2026


https://github.com/Endilll commented:

TL;DR: CWG426 was superseded by [P1787R6](https://wg21.link/p1787r6), @AaronBallman apparently got confused by an AST dump, and comments in `linkage-internal-extern.cpp` are entirely wrong.

---

> (look at the AST dump for the final declarations of i and f and see we give them external linkage while the standard now says they should have internal linkage).

If you interpreted `extern` at the end as an indication of linkage, this is how AST dump looks like with `getFormalLinkage()`:
```
|-FunctionDecl 0x55657e43d9d0 <test.cxx:1:1, col:15> col:13 f 'void ()' static internal-linkage
|-LinkageSpecDecl 0x55657e43dae0 <line:2:1, col:19> col:8 C
| `-FunctionDecl 0x55657e43db50 <col:12, col:19> col:17 h 'void ()' external-linkage
|-VarDecl 0x55657e43dc10 <line:3:1, col:16> col:12 i 'int' static cinit internal-linkage
| `-IntegerLiteral 0x55657e43dc78 <col:16> 'int' 0
`-FunctionDecl 0x55657e43dd00 <line:4:1, line:13:1> line:4:6 q 'void ()' external-linkage
  `-CompoundStmt 0x55657e43e2c8 <col:10, line:13:1>
    |-DeclStmt 0x55657e43de80 <line:5:3, col:18>
    | `-FunctionDecl 0x55657e43ddc8 parent 0x55657e3eea58 prev 0x55657e43d9d0 <col:3, col:17> col:15 f 'void ()' extern internal-linkage
    |-DeclStmt 0x55657e43df70 <line:6:3, col:18>
    | `-FunctionDecl 0x55657e43deb8 parent 0x55657e3eea58 <col:3, col:17> col:15 g 'void ()' extern external-linkage
    |-DeclStmt 0x55657e43e060 <line:7:3, col:18>
    | `-FunctionDecl 0x55657e43dfa8 parent 0x55657e3eea58 prev 0x55657e43db50 <col:3, col:17> col:15 h 'void ()' extern external-linkage
    |-DeclStmt 0x55657e43e0f8 <line:8:3, col:8>
    | `-VarDecl 0x55657e43e090 <col:3, col:7> col:7 i 'int' no-linkage
    `-CompoundStmt 0x55657e43e2a8 <line:9:3, line:12:3>
      |-DeclStmt 0x55657e43e1e8 <line:10:5, col:20>
      | `-FunctionDecl 0x55657e43e130 parent 0x55657e3eea58 prev 0x55657e43ddc8 <col:5, col:19> col:17 f 'void ()' extern internal-linkage
      `-DeclStmt 0x55657e43e290 <line:11:5, col:17>
        `-VarDecl 0x55657e43e218 parent 0x55657e3eea58 prev 0x55657e43dc10 <col:5, col:16> col:16 i 'int' extern internal-linkage
```
As you can see, the last two declarations (of `i` and `f`, respectively) have internal linkage. This was observed in our default GNU++17 mode.

When I checked a similar C example taken from N3410, the block scope extern had external linkage, as the C wording says.

> This is https://cplusplus.github.io/CWG/issues/426.html

This Core issue and its resolution were superseded by [P1787R6](https://wg21.link/p1787r6) "Declarations and where to find them", adopted in C++23 (not as a DR, but we handle the example in [[basic.link]/6](https://eel.is/c++draft/basic.link#6) the same way in C++98 mode as in GNU++17 mode).

### `extern` declarations with internal linkage in C

> Amazingly, you can declare the variable as extern in C++ and it will have internal linkage (wow)

C89 3.1.2.2 Linkage of identifiers:
> If the declaration of an identifier for an object or a function contains the storage-class specifier `extern`, the identifier has the same linkage as any visible declaration of the identifier with file scope. If there is no visible declaration with file scope, the identifier has external linkage.

```c
static int k = 0; // internal linkage
void f() {
  extern int k; // inherits internal linkage from prior visible declaration per C89 wording
}
```
```
|-VarDecl 0x55996df164b0 <test.c:1:1, col:16> col:12 k 'int' static cinit internal-linkage
| `-IntegerLiteral 0x55996df16560 <col:16> 'int' 0
`-FunctionDecl 0x55996df16620 <line:2:1, line:4:1> line:2:6 f 'void ()' external-linkage
  `-CompoundStmt 0x55996df16770 <col:10, line:4:1>
    `-DeclStmt 0x55996df16758 <line:3:3, col:15>
      `-VarDecl 0x55996df166e0 parent 0x55996deae9d8 prev 0x55996df164b0 <col:3, col:14> col:14 k 'int' extern internal-linkage
```

### Analysis of the C++ example [[basic.link]/6](https://eel.is/c++draft/basic.link#6)

Per [[dcl.meaning]/3.5](https://eel.is/c++draft/dcl.meaning#general-3.5):
> If the declaration inhabits a block scope S and declares a function ([[dcl.fct]](https://eel.is/c++draft/dcl.meaning#dcl.fct)) or uses the extern specifier, the declaration shall not be attached to a named module ([[module.unit]](https://eel.is/c++draft/module.unit)); its target scope is the innermost enclosing namespace scope, but the name is bound in S[.](https://eel.is/c++draft/dcl.meaning#general-3.5.sentence-1)

Which means declaration matching between innermost and outermost `i` succeeds ([[basic.link]/8](https://eel.is/c++draft/basic.link#8)) irrespective of `i` at block scope that has no linkage. Then innermost `i` receives the linkage of the previous declaration. This is not captured in the wording of [basic.link], but is captured as [CWG2938](https://cplusplus.github.io/CWG/issues/2938.html) "Inheriting linkage from a previous declaration". ("While the wording is in dire need of improvement here" refers to the first 4 paragraphs of [basic.link] and speaks volumes about the quality of the existing wording.)

https://github.com/llvm/llvm-project/pull/192116


More information about the cfe-commits mailing list