如何在C++17编译期检测类是否无虚基类?
Hey, let's break this down for your type-erased container scenario. You want to optimize copy operations by using std::memcpy when a type T has no virtual bases and no user-provided copy constructor—first, let's figure out how to detect the absence of virtual bases at compile time in C++17, then tie it all together for your use case.
C++'s standard library doesn't have a built-in trait for checking virtual bases directly, but we've got two solid approaches: using compiler extensions (the easiest, most reliable way) or a standard C++ trick if you need cross-compiler portability without extensions.
1. Compiler Extension Approach (Recommended)
GCC, Clang, and MSVC all provide built-in compile-time macros that can directly tell us if a type has virtual bases. We can wrap this into a reusable trait:
#include <type_traits> template <typename T> struct has_virtual_base { // GCC & Clang #if defined(__GNUC__) || defined(__clang__) static constexpr bool value = __has_virtual_base(T); // MSVC #elif defined(_MSC_VER) static constexpr bool value = __is_with_virtual_base(T); // Fallback for other compilers (adjust as needed) #else static constexpr bool value = false; #endif }; // Helper variable template for cleaner usage template <typename T> constexpr bool has_virtual_base_v = has_virtual_base<T>::value; // The check we actually need: no virtual bases template <typename T> constexpr bool is_without_virtual_base_v = !has_virtual_base_v<T>;
This is my go-to method—it's fast, uses the compiler's internal knowledge of the type, and avoids edge cases.
2. Standard C++ Trick (No Compiler Extensions)
If you can't rely on compiler extensions, we can leverage Empty Base Optimization (EBO) behavior. When a type T has no virtual bases, inheriting from it in an empty derived class won't add any extra size (thanks to EBO). But if T has virtual bases, the compiler can't optimize away the empty class's overhead, so the derived class will be larger than T.
Here's the code:
#include <type_traits> template <typename T> struct EmptyDerived : public T {}; template <typename T> constexpr bool is_without_virtual_base_v = sizeof(T) == sizeof(EmptyDerived<T>);
⚠️ Heads up: This has limitations. If T is an empty class, sizeof(T) and sizeof(EmptyDerived<T>) will both be 1 (the minimum size for empty classes in C++), leading to a false positive. Use this only if you know T won't be empty.
Now, let's tie this to your goal of using std::memcpy. A quick note: your original condition (no virtual bases + no user-provided copy constructor) is a starting point, but to be safe, you should really check if T is trivially copyable—this ensures not just that the copy constructor is compiler-generated, but that all members are also trivially copyable (so std::memcpy won't break things like std::string or other non-trivial types).
That said, let's cover both your requested condition and the safer trivial copy check:
Checking for No User-Provided Copy Constructor
We can use std::is_trivially_copy_constructible as a proxy here—if a type has a user-provided copy constructor, this trait will return false. For compiler-generated copy constructors, it returns true only if the copy is trivial (which aligns with the safe std::memcpy use case).
// Check if T has a user-provided copy constructor template <typename T> constexpr bool has_user_defined_copy_constructor_v = !std::is_trivially_copy_constructible_v<T>; // Your requested condition: no virtual bases + no user-provided copy constructor template <typename T> constexpr bool can_use_memcpy_v = is_without_virtual_base_v<T> && !has_user_defined_copy_constructor_v<T>; // Safer condition: trivially copyable + no virtual bases (covers all safe memcpy cases) template <typename T> constexpr bool safe_to_use_memcpy_v = std::is_trivially_copyable_v<T> && is_without_virtual_base_v<T>;
Putting it all together with test cases:
#include <type_traits> #include <cstring> // Virtual base check (compiler extension version) template <typename T> struct has_virtual_base { #if defined(__GNUC__) || defined(__clang__) static constexpr bool value = __has_virtual_base(T); #elif defined(_MSC_VER) static constexpr bool value = __is_with_virtual_base(T); #else static constexpr bool value = false; #endif }; template <typename T> constexpr bool has_virtual_base_v = has_virtual_base<T>::value; template <typename T> constexpr bool is_without_virtual_base_v = !has_virtual_base_v<T>; // Copy constructor checks template <typename T> constexpr bool has_user_defined_copy_constructor_v = !std::is_trivially_copy_constructible_v<T>; template <typename T> constexpr bool can_use_memcpy_v = is_without_virtual_base_v<T> && !has_user_defined_copy_constructor_v<T>; template <typename T> constexpr bool safe_to_use_memcpy_v = std::is_trivially_copyable_v<T> && is_without_virtual_base_v<T>; // Test types class NoVirtualBase { int x; }; class WithVirtualBase : virtual public NoVirtualBase { int y; }; class UserDefinedCopy { public: UserDefinedCopy(const UserDefinedCopy&) {} // User-provided copy ctor }; class TrivialWithNonTrivialMember { std::string s; // Non-trivial member, so compiler-generated copy ctor is non-trivial }; int main() { // Virtual base checks static_assert(is_without_virtual_base_v<NoVirtualBase>, "NoVirtualBase has no virtual bases"); static_assert(!is_without_virtual_base_v<WithVirtualBase>, "WithVirtualBase has a virtual base"); // Memcpy eligibility checks static_assert(can_use_memcpy_v<NoVirtualBase>, "NoVirtualBase qualifies for memcpy"); static_assert(!can_use_memcpy_v<WithVirtualBase>, "WithVirtualBase has virtual bases"); static_assert(!can_use_memcpy_v<UserDefinedCopy>, "UserDefinedCopy has a user-provided copy ctor"); static_assert(!safe_to_use_memcpy_v<TrivialWithNonTrivialMember>, "Non-trivial member makes memcpy unsafe"); // Example usage in your container if constexpr (safe_to_use_memcpy_v<NoVirtualBase>) { NoVirtualBase a{42}; NoVirtualBase b; std::memcpy(&b, &a, sizeof(NoVirtualBase)); } else { // Fallback to regular copy constructor NoVirtualBase a{42}; NoVirtualBase b{a}; } }
Even if a type has no virtual bases and no user-provided copy constructor, using std::memcpy is only safe if the type is trivially copyable. Non-trivial members (like std::string) will break if you memcpy them—so I strongly recommend using the safe_to_use_memcpy_v check instead of the more permissive one you initially described.
内容的提问来源于stack exchange,提问作者JiaHao Xu

