56 Handling name collisions — from prefixes to namespace
What to know first
Looking back
Chapter 55 said C has only four name spaces and that you cannot make a new one. What happens, then, when two libraries that know nothing of each other each export a function called init?
A. If both have external linkage the linker refuses with a multiple definition — and that is the lucky case. The unlucky one is worse. If one of them sits inside a static library, the linker takes the first one it finds and moves on in silence. Compiling and linking both succeed, and the wrong function is called.
So the answer to this problem is not “fix it when it collides” but “make a collision impossible in the first place.” This chapter is those methods.
The need for this chapter, and its context
By the end of this chapter
namespace, and what to watch for when the two languages are mixed.The questions this chapter answers
- Is
multiple definitionactually the welcome error? - How do I check what I am actually exporting?
- Should C then bring name spaces into the language?
56.1 When and where collisions go off
Spellings collide in three places, and each is harder to diagnose than the last.
| When | What tells you | How easy to handle |
|---|---|---|
| At compile time | declared twice in one scope — the compiler refuses at once | Easy. It is visible right there |
| At link time | multiple definition of 'init' | Moderate. The message names the files |
| Silently | nothing tells you | Hard. The wrong function runs and you see only symptoms |
Table 57.1
The third is why this chapter exists. Three places where silent collisions arise.
- The link order of static libraries. If
libfoo.aandlibbar.aboth carry the name, the linker takes whichever it met first on the command line and stops. Reorder the libraries and the program behaves differently. - Weak symbols. A name the implementation marked “anyone may override this” is replaced without a word. Swapping in your own
mallocis this pattern. - Interception at run time. Unix’s
LD_PRELOADlets whatever loaded first win. A legitimate technique for diagnostic tools, and an accident when the same name is used by chance.
Q. Is multiple definition actually the welcome error?
A. Yes. It is an audible collision. What is genuinely dangerous is the linker picking one side without a word.
So the direction of the discipline is not “let us fix collisions well” but “let us make sure a collision is heard.” Cut the exported names to a minimum and (a) the chance of colliding drops and (b) when one happens the linker makes a noise. All four weapons below point that way.
56.2 The first weapon — do not export (static)
The surest defence is not putting the name outside at all. Add static to a file-scope declaration and it has internal linkage (chapter 54): outside that translation unit it does not exist.
The discipline reduces to one line — make static the default and release only what goes in the header. There is a bonus: knowing “this name cannot be called from another file”, the compiler can optimise more aggressively (inlining, removing unused functions).
Counter-example. Leaving something global that was never meant for the header
/* util.c */
int helper(int x) { … } /* not in any header, yet external */Nobody calls it, and still it occupies a name in the linker’s yard. The day another file happens to make the same name, the problem surfaces. The cost of writing static is one word.
56.3 The second weapon — the prefix convention
For names that must be exported, the fence is built by hand. It is the universal practice of C libraries, and the larger the project the fewer the exceptions.
| Project | Prefix | How far it goes |
|---|---|---|
| SQLite | sqlite3_ | functions, types, constants — even the version number is in the prefix |
| libcurl | curl_, CURL | lowercase for functions, capitals for constants and types |
| zlib | z, inflate/deflate | old enough that the prefix is short — which is why it sometimes collides |
| OpenSSL | SSL_, EVP_, X509_ | a different prefix per module |
| GLib, GTK | g_, gtk_, G_, GTK_ | types in camel case, as GtkWidget |
| SDL | SDL_ | functions, types, constants and macros alike |
| The Linux kernel | the subsystem’s name | kmalloc, vfs_, sock_ — the layer as the prefix |
Table 57.2
Four design rules can be read out of it.
- Short, but unique. zlib’s
zis convenient for being short and collides just as easily. Three or four characters is a safe choice today. - Put it on types and macros too. A prefix that covers only functions is half a fence — chapter 55 showed that type names and enumeration constants live in the same yard.
- Split the layers by case. Combine the prefix with the habit of lowercase for functions and capitals for macros and constants, and a name alone reveals what it is.
- Write it down. One line in the conventions document saying “our prefix is this” is cheaper than pointing it out a hundred times in review.
In practice. The legacy of the days before prefixes
The standard library is itself a museum of unprefixed names — open, read, write, time, index, link. In the 1970s a program used a handful of libraries, so it was not a problem.
Today’s common accident is the result. Make a function called read or time in your own code and you hide the standard one, and other code calling it quietly goes elsewhere. That is what chapter 55′s table of reserved names is for, and “assume the short, common words are already taken” is practice’s first rule.
56.4 The third weapon — symbol visibility
A prefix is a convention people keep, so it leaks. There is one more layer that lets the build enforce it — the symbol visibility of a shared library.
| Platform | Hiding by default | Marking what is exported |
|---|---|---|
| ELF (Linux, BSD) | -fvisibility=hidden | __attribute__((visibility("default"))) |
| macOS | -fvisibility=hidden | the same attribute, or a list file of exported symbols |
| Windows | already hidden by default | __declspec(dllexport) or a .def file |
Table 57.3
The practice is to wrap it in one macro.
#if defined(_WIN32)
# define MYLIB_API __declspec(dllexport)
#elif defined(__GNUC__)
# define MYLIB_API __attribute__((visibility("default")))
#else
# define MYLIB_API
#endif
MYLIB_API int mylib_open(const char *path);Three effects: collisions grow rarer, loading gets faster (fewer symbols to resolve), and the boundary between the internal functions you may change and the public ones you may not is written into the code.
Q. How do I check what I am actually exporting?
A. Count them. Pull the defined global symbols out of the object file or library.
nm -g --defined-only mylib.o # an object file
nm -D --defined-only libmy.so # a shared library (dynamic symbols)
objdump -T libmy.so # the same job with another toolMeasure this chapter’s listing that way and the exported names are just two, main and textbuf — the inner functions are all static and absent from the list.
One good habit: dump the list of public symbols to a file and compare it in the build. If a new name leaks out unintended, the build says so. It is rung 3 of chapter 12′s ladder (let the build tell you) applied to names.
56.5 The fourth weapon — cut down the names themselves
Better than building a good fence is having few names to put outside. There is a pattern in which a module, instead of exporting twenty functions, exports a single struct holding function pointers.
examples/ch56/prefix.c
/* 「모듈 하나 = 외부 심볼 하나」 — 접두어 규약과 함수 포인터 표.
C 에는 사용자가 만드는 이름 공간이 없으니, 내보내는 이름을 줄이는 것이
가장 확실한 방어다. */
#include <stdio.h>
#include <string.h>
/* ── 안쪽 구현은 전부 내부 연결(static) — 바깥 마당에 나가지 않는다 ── */
static int buf_len;
static char buf[64];
static void impl_reset(void) { buf_len = 0; buf[0] = '\0'; }
static bool impl_push(const char *s)
{
size_t n = strlen(s);
if ((size_t)buf_len + n + 1 > sizeof buf) return false; /* 잘림은 실패다(43장) */
memcpy(buf + buf_len, s, n + 1);
buf_len += (int)n;
return true;
}
static const char *impl_text(void) { return buf; }
/* ── 밖에 내보내는 것은 이것 하나 ──
표(vtable) 하나에 접두어를 붙여 두면, 이 번역 단위가 링커에 내미는
이름은 `textbuf` 단 하나다. 나머지 철자는 다른 파일과 부딪힐 수 없다. */
struct textbuf_api {
void (*reset)(void);
bool (*push)(const char *);
const char *(*text)(void);
};
const struct textbuf_api textbuf = {
.reset = impl_reset,
.push = impl_push,
.text = impl_text,
};
/* ── 쓰는 쪽 ── */
int main(void)
{
textbuf.reset();
puts("[this module exposes exactly one external name: `textbuf`]");
printf(" push(\"hello \") -> %s\n", textbuf.push("hello ") ? "ok" : "failed");
printf(" push(\"world\") -> %s\n", textbuf.push("world") ? "ok" : "failed");
printf(" text() -> \"%s\"\n", textbuf.text());
char big[80];
memset(big, 'A', sizeof big - 1);
big[sizeof big - 1] = '\0';
printf(" pushing a long string -> %s (truncation does not count as success)\n",
textbuf.push(big) ? "ok" : "failed");
puts("\n[the prefix convention - what large projects do]");
puts(" sqlite3_ / curl_ / SSL_ / g_ / SDL_ ... the same prefix goes on types and macros");
puts(" too. A prefix is a name space that C lacks, supplied by hand.");
return 0;
}
Output
[this module exposes exactly one external name: `textbuf`]
push("hello ") -> ok
push("world") -> ok
text() -> "hello world"
pushing a long string -> failed (truncation does not count as success)
[the prefix convention - what large projects do]
sqlite3_ / curl_ / SSL_ / g_ / SDL_ ... the same prefix goes on types and macros
too. A prefix is a name space that C lacks, supplied by hand.
The only name this translation unit hands the linker is textbuf (the nm measurement above). The calling side writes textbuf.push(...) with a dot — and the bonus of the pattern is that it reads like a language that has name spaces.
The price is worth stating plainly.
| What you gain | What you pay |
|---|---|
| external names collapse to one | calls go through a function pointer — inlining gets hard |
| the implementation can be swapped whole | one level of indirect call slower |
| the caller reads like a name space | a debugger no longer shows the callee at a glance |
Table 57.4
So the pattern belongs at boundaries — plugins, swappable back ends, test doubles. Not in an inner loop where speed matters.
56.6 How C++ solved this spot
C++ put “the user digs a yard” into the language. Here is as much as a C programmer needs, and accurately.
56.6.1 The syntax
namespace app {
int parse(const char *);
namespace detail { int helper(int); } // nested
}
namespace app::io { void flush(); } // the C++17 shorthand
int x = app::parse("42");
int y = app::detail::helper(1);The yard’s name goes in front of the name. It does the same job as C’s prefix convention; what differs is that the language enforces it and the tools understand it.
56.6.2 The anonymous namespace — the equivalent of C’s static
namespace { int hidden(int x) { return x; } } // this translation unit onlyMeasuring shows the character of it.
| Declaration | Symbol kind (nm) | Meaning |
|---|---|---|
static int st(int); | t — local | internal linkage, as in C |
hidden in an anonymous namespace | t — local | the same effect; the name prints as (anonymous namespace)::hidden |
an ordinary int use(int); | T — global | external linkage |
Table 57.5
The two have the same effect, but C++ prefers the anonymous namespace — unlike static it can be applied to types as well, and those can be passed as template arguments.
56.6.3 using namespace — the convenience and its price
using namespace std; tears down the yard’s fence on the spot. Handy in a short example; the conventions of practice mostly run like this.
| Convention | Ground |
|---|---|
| Never in a header | the fence comes down for every file that includes it |
| Narrow even in a source file | one name at a time, as using std::string; |
| Inside a function | the effect is confined to that function |
swap is the exception — it has its own idiom | because of ADL, below |
Table 57.6
56.6.4 ADL — looking in the yard of the argument
C++ has argument-dependent lookup: the yard where the argument’s type lives is searched as well, automatically.
namespace app { struct Buf {}; void print(const Buf &); }
app::Buf b;
print(b); // app:: was not written, yet app::print is called — ADLConvenient, and a surprising place too. Which function gets called depends on the type of the argument, so in code with many overloads a person struggles to follow it. C has no such lookup — one name is one function. Worth remembering as a spot where C’s simplicity pays.
56.6.5 Name mangling and extern "C"
C++ supports overloading, so the same spelling with different argument types is a different function. For that, the name the linker sees must carry type information — that is mangling.
| Source | The name the linker sees (GCC, measured) |
|---|---|
namespace app { int f(int); } | _ZN3app1fEi |
namespace app { double f(double); } | _ZN3app1fEd |
extern "C" int c_f(int); | c_f |
Table 57.7
Take _ZN3app1fEi apart and the yard name app (three characters), the function name f (one) and the argument type i are all in there. The name space is carved into the symbol.
So mixing C and C++ means saying “name this function by C’s rules”, and that is extern "C". The canonical pattern for a C header:
#ifdef __cplusplus
extern "C" {
#endif
int mylib_open(const char *path);
#ifdef __cplusplus
}
#endifPlatform note. Where a C header breaks in C++ alone
Some headers are fine in C and will not compile in C++. The cause is nearly always one thing — a C++ keyword used as a name.
int register_thing(int class, int new); /* C: fine. C++: an error */Measured, the C compiler says nothing and the C++ compiler refuses with expected primary-expression before 'int'.
So when writing a header that may also be used from C++, courtesy is to keep C++‘s reserved words (class, new, delete, template, this, namespace, try, catch, operator, private, public, virtual and the rest) out of parameter names. Append an underscore (class_) or pick another word.
Words that became keywords in C23 — bool, true, false — also turn up as variable names in old C headers. The same family of problem.
Q. Should C then bring name spaces into the language?
A. It has been proposed several times and folded each time for the same reason — the ABI and existing code. Bringing in name spaces means the yard must be carved into the symbol (the mangling above), and that splits forty years of libraries and linking conventions. C survives today as “the common denominator any language can call” precisely because its symbol names are simple (chapter 96).
In other words this is not a deficiency but a trade. C gave up convenience in names and gained simplicity in linking. That simplicity is why Python, Java and Rust all speak C’s ABI.
So this chapter’s weapons are not stopgaps. They match C’s design, and large projects have got along on them for decades.
56.7 Good habits — a summary
| Habit | Why |
|---|---|
Make static the default | a name never exported cannot collide |
| Settle a prefix and write it down | a fence built by people needs agreement to stand |
| Prefix types, macros and enumeration constants too | all three share one yard (ch. 55) |
| Avoid short, common words | read, time, index already have owners |
| Let the build enforce visibility | tools plug what conventions leak |
| Dump the public symbol list and compare it | the moment one leaks, the build says so |
| Keep C++ keywords out of headers | you cannot know everywhere they will be used |
Turn on -Wshadow | shadowing is not caught by the default warnings (ch. 55) |
Table 57.8
We know how to govern names. But chapter 55 went past the remark that “macros ignore all four name spaces” — meaning a layer that knows nothing of C replaces names. The next chapter opens that layer head on, and follows the formal steps by which source code becomes a program.