Why std::is_copy_constructible Returns True for std::vector<std::mutex> in C++17 (And How to Fix It)
The Problem: The False Positive of std::is_copy_constructible
If you have ever written template metaprogramming code in C++17 to conditionally enable features based on whether a type is copy-constructible, you might have run into a baffling issue:
#include <type_traits>
#include <vector>
#include <mutex>
// std::mutex cannot be copied, yet this evaluates to true!
static_assert(std::is_copy_constructible_v<std::vector<std::mutex>> == true);When you actually attempt to copy a std::vector<std::mutex>, the compiler throws a hard compilation error deep inside the standard library headers. So why does std::is_copy_constructible_v claim the type can be copied?
Why Does This Happen? (Immediate Context vs. Instantiation)
To understand this behavior, we need to understand how SFINAE (Substitution Failure Is Not An Error) and type traits work:
std::is_copy_constructible<T>checks whether the expressionT(std::declval<const T&>())is well-formed within the immediate context of the type declaration.- For
std::vector<T>, the copy constructorvector(const vector&)is unconditionally declared in the class definition. It is not a member template, nor is it conditionally enabled with SFINAE. - The failure only occurs when the compiler attempts to instantiate the body of
std::vector's copy constructor, which tries to copy eachT(in this case,std::mutex).
Because SFINAE cannot peek into function bodies or non-immediate contexts, the type trait concludes that the copy constructor exists and therefore evaluates to true.
Can C++17 Automatically Detect Ill-Formed Constructor Bodies?
In pure C++, there is no universal, generic trait that can detect whether an unconditionally declared function will fail during instantiation. An ill-formed function body is a hard compile-time error, not a substitution failure.
However, you can solve this in C++17 by building a custom, "deep" constructibility trait that specializes standard container types to ensure their contained elements are also copy-constructible.
The Solution: Implementing a Deep Copy Trait in C++17
We can define a custom trait, is_deeply_copy_constructible, which falls back to std::is_copy_constructible for generic types, but adds specialized inspections for standard containers like std::vector, std::deque, std::list, std::pair, and std::tuple.
#include <iostream>
#include <type_traits>
#include <vector>
#include <mutex>
#include <utility>
#include <tuple>
// Primary template: falls back to standard trait
template <typename T, typename = void>
struct is_deeply_copy_constructible : std::is_copy_constructible<T> {};
template <typename T>
inline constexpr bool is_deeply_copy_constructible_v = is_deeply_copy_constructible<T>::value;
// Specialization for std::vector
template <typename T, typename Alloc>
struct is_deeply_copy_constructible<std::vector<T, Alloc>>
: is_deeply_copy_constructible<T> {};
// Specialization for std::pair
template <typename T1, typename T2>
struct is_deeply_copy_constructible<std::pair<T1, T2>>
: std::bool_constant<is_deeply_copy_constructible_v<T1> && is_deeply_copy_constructible_v<T2>> {};
// Specialization for std::tuple
template <typename... Ts>
struct is_deeply_copy_constructible<std::tuple<Ts...>>
: std::bool_constant<(is_deeply_copy_constructible_v<Ts> && ...)> {};
// Verification
struct NonCopyable {
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
};
int main() {
// Standard trait false positives:
static_assert(std::is_copy_constructible_v<std::vector<std::mutex>> == true);
static_assert(std::is_copy_constructible_v<std::vector<NonCopyable>> == true);
// Deep trait correctly reports false:
static_assert(is_deeply_copy_constructible_v<std::vector<std::mutex>> == false);
static_assert(is_deeply_copy_constructible_v<std::vector<NonCopyable>> == false);
static_assert(is_deeply_copy_constructible_v<std::vector<int>> == true);
std::cout << "All assertions passed successfully!\n";
return 0;
}How to Generalize This for Move and Custom Constructors
You can follow the exact same recursive specialization pattern for other operations such as move-construction or default-construction:
- Create an
is_deeply_move_constructible<T>that checksis_deeply_move_constructible<typename Vector::value_type>. - If you develop your own generic template wrappers (such as a custom
Optional<T>orResult<T>), use SFINAE on the constructor declaration itself so standard traits work out-of-the-box without needing manual specializations:
template <typename T>
class MyWrapper {
public:
// Conditionally enable the copy constructor via SFINAE
template <typename U = T, typename = std::enable_if_t<std::is_copy_constructible_v<U>>>
MyWrapper(const MyWrapper& other) {
// ...
}
};Summary
Because the C++ compiler cannot inspect function bodies during template argument substitution, std::is_copy_constructible_v will continue to report true for containers holding non-copyable types in C++17. The cleanest workaround is to define a recursive custom trait that validates nested element types before relying on container-level operations.