Why Isn't std::pair Trivially Copyable? Understanding C++ ABI and Performance Constraints
The Enigma of std::pair and Trivial Copyability
In modern C++, performance optimization often relies on type traits. One of the most impactful traits is std::is_trivially_copyable. When a type is trivially copyable, standard containers like std::vector can bypass individual move/copy constructors during reallocation and instead use high-speed memory operations like std::memcpy or std::memmove.
Given that std::pair<int, int> is logically just two consecutive integers in memory, developers naturally expect it to be trivially copyable. Yet, depending on your standard library version and language standard, std::is_trivially_copyable_v<std::pair<int, int>> has historically evaluated to false. Why was std::pair designed this way?
1. The Root Cause: User-Declared Constructors
According to the C++ Standard, a class is trivially copyable only if:
- It has at least one eligible copy constructor, move constructor, copy assignment operator, or move assignment operator.
- Each eligible copy/move constructor and assignment operator is trivial (either implicitly declared or explicitly defaulted on its first declaration).
- It has a trivial, non-deleted destructor.
Historically, std::pair needed to support complex templated conversions, explicit/implicit conversion rules, and piecewise construction. To achieve this in C++98 through C++17, standard library implementations defined explicit constructor templates and non-defaulted special member functions.
// Simplified illustration of traditional std::pair implementation
template <typename T1, typename T2>
struct pair {
T1 first;
T2 second;
// User-declared constructor inhibits trivial copyability
pair(const pair& other)
: first(other.first), second(other.second) {}
pair& operator=(const pair& other) {
first = other.first;
second = other.second;
return *this;
}
};Because the copy constructor and copy assignment operator were user-provided, the compiler was required by the C++ specification to mark the type as non-trivially copyable.
2. The ABI (Application Binary Interface) Dilemma
Why didn't standard library vendors simply replace user-provided constructors with = default once C++11 introduced defaulted functions?
The answer lies in ABI stability. In many platform ABIs (such as the System V AMD64 ABI used by Linux and macOS):
- Trivially copyable types of small size (e.g., two 32-bit integers totaling 8 bytes) are passed directly in CPU registers (like
%raxor%rdi). - Non-trivially copyable types must be passed in memory via hidden pointer references on the stack.
If a compiler or standard library vendor (such as GCC's libstdc++ or LLVM's libc++) changed std::pair from non-trivial to trivial, the function calling convention for any function accepting or returning std::pair<int, int> would instantly change. Binaries compiled with an older compiler version would become incompatible with binaries compiled with the new version, causing segmentation faults and silent data corruption.
3. How Standard Libraries Optimize Vector Relocation Anyway
Even when std::pair<int, int> was not strictly trivially copyable under the standard trait, standard library implementers introduced internal extensions to maintain performance.
Libraries like GCC's libstdc++ and Clang's libc++ utilize internal concepts known as trivial relocatability (e.g., __is_bitwise_relocatable or __is_trivially_relocatable). When resizing a std::vector<std::pair<int, int>>, the vector implementation queries these compiler intrinsics and safely utilizes memcpy behind the scenes.
#include <iostream>
#include <type_traits>
#include <utility>
int main() {
// In C++20 and newer with updated standard libraries:
std::cout << std::boolalpha;
std::cout << "Is pair<int, int> trivially copyable? "
<< std::is_trivially_copyable_v<std::pair<int, int>>
<< std::endl;
return 0;
}4. The Modern C++ Resolution (C++20 and C++23)
Recent revisions to the standard (starting with C++20 and refined through papers like P2321R2 in C++23) modernized std::pair and std::tuple. Thanks to conditional defaulting via explicit(bool) and conditional triviality:
- Modern standard library releases (such as GCC 11+, Clang 13+, and MSVC STL) now implement
std::pairsuch thatstd::is_trivially_copyable_v<std::pair<T1, T2>>istruewhenever bothT1andT2are trivially copyable. - Where ABI breaks were unavoidable, vendors carefully gated changes behind new ABI flags or coordinated updates across major toolchain transitions.
Summary and Best Practices
- Legacy constraints:
std::pairwas not trivially copyable because user-declared constructors and operators were needed for conversions before modern language features existed. - ABI concerns: Updating these definitions altered parameter-passing calling conventions at machine level.
- Custom lightweight structs: If you are targeting legacy environments and need guaranteed trivial copyability without standard library overhead, define a plain aggregate struct:
struct IntPair {
int first;
int second;
};
static_assert(std::is_trivially_copyable_v<IntPair>);