C++ Code Hygiene
Nicolae Vartolomei · 2025/02, updated on 2025/02
Table of Contents
Your Rights End Where Mine Begin
Template specializations should reside in the same header file that defines the template or at least one of the types for which the template is specialized.
Therefore, as a library author, you should only specialize templates defined within the same library, or specialize third-party templates for types you have defined. Avoid specializing third-party templates for types defined in other libraries.
Violating this rule is an easy path to ODR violations.
Example violation
// lib.hpp
#pragma once
struct A {};
// lib_ext.hpp
#pragma once
#include "lib.hpp"
#include <functional>
template <> struct std::hash<A> {
std::size_t operator()(const A &a) const { return 0; }
};
// lib.cpp
#include "lib.hpp"
#include "lib_ext.hpp"
#include <iostream>
void f() {
std::cout << "hashes to " << std::hash<A>{}(A{}) << "\n";
}
// main.cpp
#include "lib.hpp"
#include <iostream>
template<>
struct std::hash<A> {
std::size_t operator()(const A &a) const { return 42; }
};
int main() {
std::cout << "hashes to " << std::hash<A>{}(A{}) << "\n";
return 0;
}
Compile the source files:
$ echo "lib main" | xargs -n1 -I{} clang++ --std=c++23 -c {}.cpp -o {}.o
Link the object files and execute:
$ clang++ main.o lib.o -o main && ./main
hashes to 42
Changing the linking order may produce different results ☢️:
$ clang++ lib.o main.o -o main && ./main
hashes to 0
Adhering to the rule would have produced a more desirable outcome, namely a compilation error.
// lib.hpp
#pragma once
#include <functional>
struct A {};
template <> struct std::hash<A> {
std::size_t operator()(const A &a) const { return 0; }
};
$ echo "lib main" | xargs -n1 -I{} clang++ --std=c++23 -c {}.cpp -o {}.o
main.cpp:6:13: error: redefinition of 'hash<A>'
6 | struct std::hash<A> {
| ^~~~~~~
./lib.h:7:25: note: previous definition is here
7 | template <> struct std::hash<A> {
| ^
1 error generated.
Related reading: https://gudok.xyz/inline/
Standardized behavior for when rules are violated
6.3 One-definition rule
…
14. For any definable item D with definitions in multiple translation units,
14.1. — if D is a non-inline non-templated function or variable, or
14.2. — if the definitions in different translation units do not satisfy the following requirements,
the program is ill-formed; a diagnostic is required only if the definable item is attached to a named module and a prior definition is reachable at the point where a later definition occurs.
… (see the standard for exact requirements but summary is that all definitions should be “indetical”)
— (C++23) Working Draft, Standard for Programming Language C++
Specializing Standard Library: Know Your Boundaries
C++ standard explicitly calls this out for the standard library. This way, the
standard library can be evolved without breaking compliant user code. If this
clause were not present, the standard library would be unable to add new
specializations to existing templates/types within the standard library. For
example, if you as a user have a specialization for
std::hash<std::chrono::...>
—your fault:
P2592R3: Hashing support for std::chrono value classes
is coming.
16.4.5.2.1 Namespace std
…
2. Unless explicitly prohibited, a program may add a template specialization for any standard library class template to namespacestd
provided that (a) the added declaration depends on at least one program-defined type and (b) the specialization meets the standard library requirements for the original template.
— (C++23) Working Draft, Standard for Programming Language C++
Specializing {fmt}: Know Your Boundaries
Let’s consider {fmt}. If you specialize fmt::formatter<T>
for a type T
that is not defined in your library, you are violating the
library’s boundaries. This is because {fmt} library may add specializations for
types that are not defined in your library.
I.e. it is convenient to add specializations for things like std::chrono::...
,
std::optional
, std::variant
, etc. in your library because for a long time
{fmt} did not provide them. But now it does via fmt/chrono.h
, fmt/std.h
,
etc. So now you end up with ODR violations.