如何强制Base类使用者新增枚举ID并通过SubclassFactory创建实例?
Great question! Let's tackle both requirements with compile-time enforcement first (since that's the optimal solution) and complement with unit tests for extra safety.
1. Force Instance Creation Only via SubclassFactory
The cleanest compile-time way to prevent direct instantiation of subclasses is using a private token pattern. This ensures only the factory can call the constructor of Base (and thus subclasses):
#include <memory> #include <stdexcept> // Forward declare factory to make it a friend class SubclassFactory; class Base { private: // Private token only accessible to friends struct FactoryToken { explicit FactoryToken() = default; FactoryToken(const FactoryToken&) = delete; FactoryToken& operator=(const FactoryToken&) = delete; }; // Grant factory exclusive access to the token friend class SubclassFactory; protected: // Protected constructor requires the token explicit Base(FactoryToken) {} public: virtual ~Base() = default; // Add your base class methods here }; // Example subclass class SubclassA : public Base { protected: // Subclass constructor also requires the token (called by factory) explicit SubclassA(FactoryToken token) : Base(std::move(token)) {} friend class SubclassFactory; }; enum class SubclassId { A, // Add new enum entries here when creating subclasses }; class SubclassFactory { public: static std::unique_ptr<Base> create(SubclassId id) { switch (id) { case SubclassId::A: return std::make_unique<SubclassA>(Base::FactoryToken{}); default: throw std::invalid_argument("Unknown SubclassId"); } } };
Why this works:
- Any attempt to instantiate
SubclassAdirectly (e.g.,new SubclassA()) will fail at compile time: the constructor is protected, and you can't createFactoryToken(it's private toBase). - Only
SubclassFactorycan create the token and pass it to the constructor, ensuring all instances go through the factory.
2. Force Adding SubclassId Entry for New Subclasses
To enforce this at compile time, we'll use CRTP (Curiously Recurring Template Pattern) and compile-time assertions with a type-to-enum trait. Here's how:
Step 1: Add CRTP to Base and Type-to-Enum Trait
Modify the Base class to use CRTP, and create a trait that maps each subclass to its corresponding SubclassId:
#include <array> #include <tuple> #include <type_traits> // Enum definition enum class SubclassId { A, B // New entry for SubclassB }; // Trait to map subclass types to their SubclassId template<typename Derived> struct SubclassToId; // Specialization for SubclassA template<> struct SubclassToId<SubclassA> { static constexpr SubclassId value = SubclassId::A; }; // CRTP Base class template<typename Derived> class Base { private: struct FactoryToken { /* same as before */ }; friend class SubclassFactory; protected: explicit Base(FactoryToken) {} public: virtual ~Base() = default; // Compile-time check: Derived must have a valid SubclassToId specialization static_assert( std::is_same_v<decltype(SubclassToId<Derived>::value), SubclassId>, "ERROR: You must specialize SubclassToId for this subclass with a valid SubclassId" ); // Get the subclass ID from the trait static constexpr SubclassId getId() { return SubclassToId<Derived>::value; } }; // Update SubclassA to inherit from CRTP Base class SubclassA : public Base<SubclassA> { protected: explicit SubclassA(FactoryToken token) : Base(std::move(token)) {} friend class SubclassFactory; }; // New subclass example class SubclassB : public Base<SubclassB> { protected: explicit SubclassB(FactoryToken token) : Base(std::move(token)) {} friend class SubclassFactory; }; // Specialize SubclassToId for SubclassB (required!) template<> struct SubclassToId<SubclassB> { static constexpr SubclassId value = SubclassId::B; };
Step 2: Compile-Time Check for Full Enum Coverage
To ensure every SubclassId has a corresponding subclass (and vice versa), add these compile-time assertions:
// Compile-time list of all SubclassId values constexpr std::array<SubclassId, 2> ALL_SUBCLASS_IDS = {SubclassId::A, SubclassId::B}; // Compile-time tuple of all subclass types using ALL_SUBCLASSES = std::tuple<SubclassA, SubclassB>; // Helper to get all subclass IDs as a compile-time array template<typename Tuple> constexpr auto get_subclass_ids() { return []<std::size_t... I>(std::index_sequence<I...>) { return std::array<SubclassId, sizeof...(I)>{ SubclassToId<std::tuple_element_t<I, Tuple>>::value... }; }(std::make_index_sequence<std::tuple_size_v<Tuple>>{}); } constexpr auto SUBCLASS_IDS_FROM_TYPES = get_subclass_ids<ALL_SUBCLASSES>(); // Check 1: Every SubclassId has a corresponding subclass static_assert([]{ for (auto enum_id : ALL_SUBCLASS_IDS) { bool found = false; for (auto subclass_id : SUBCLASS_IDS_FROM_TYPES) { if (enum_id == subclass_id) { found = true; break; } } if (!found) return false; } return true; }(), "ERROR: All SubclassId entries must have a corresponding subclass"); // Check 2: Every subclass has a unique, valid SubclassId static_assert([]{ for (auto subclass_id : SUBCLASS_IDS_FROM_TYPES) { bool found = false; for (auto enum_id : ALL_SUBCLASS_IDS) { if (subclass_id == enum_id) { found = true; break; } } if (!found) return false; } // Also check for duplicate IDs for (std::size_t i = 0; i < SUBCLASS_IDS_FROM_TYPES.size(); ++i) { for (std::size_t j = i+1; j < SUBCLASS_IDS_FROM_TYPES.size(); ++j) { if (SUBCLASS_IDS_FROM_TYPES[i] == SUBCLASS_IDS_FROM_TYPES[j]) return false; } } return true; }(), "ERROR: All subclasses must have a unique, valid SubclassId entry");
Why this works:
- If you create a new subclass without specializing
SubclassToId, thestatic_assertinBase<Derived>will throw a compile error. - If you add a
SubclassIdentry but forget to add the corresponding subclass toALL_SUBCLASSES, the first static_assert fails. - If you assign a duplicate or invalid
SubclassIdto a subclass, the second static_assert catches it.
Complementary Unit Test
For extra safety (especially if you're working with a team), add a unit test that verifies every SubclassId can be instantiated via the factory:
#include <gtest/gtest.h> TEST(SubclassFactoryTest, AllEnumValuesCanBeCreated) { // Iterate over all SubclassId values for (const auto id : ALL_SUBCLASS_IDS) { EXPECT_NO_THROW(SubclassFactory::create(id)); ASSERT_NE(SubclassFactory::create(id), nullptr); } }
This test will fail if someone adds an enum entry but forgets to update the factory's switch statement.
内容的提问来源于stack exchange,提问作者Egor Okhterov

