SFINAE, if constexpr & partial specialization for SFRegT
SFRegT with compile-time and runtime register addresses using SFINAE, if constexpr, and partial specialization. Code examples, patterns, and trade-offs.
How can I create a single C++ template class SFRegT with two variants to avoid code duplication: one templated on a compile-time uintptr_t register address (no constructor or member variable needed), and another non-templated using a constexpr constructor with a runtime uintptr_t parameter and private member variable? Both should share the same member functions like Reset(), Set(uint32_t), Get(), setBit(uint16_t), etc.
Templated variant example:
template<uintptr_t mRegAddr>
class SFRegT {
public:
void Reset() const {
*(volatile uint32_t*)mRegAddr = 0;
}
void Set(uint32_t Value) const {
*(uint32_t*)mRegAddr = Value;
}
uint32_t Get() const {
return *(volatile uint32_t*)mRegAddr;
}
uint32_t operator()() const { return Get(); }
void setBit(const uint16_t BitPos) const {
setSFRBit(mRegAddr, BitPos);
}
// other members
};
Desired non-templated variant:
class SFRegT {
public:
constexpr SFRegT(uintptr_t RegAddr) : mRegAddr(RegAddr) {}
// same member functions as above, using mRegAddr
private:
const uintptr_t mRegAddr;
};
Can this be achieved using partial template specialization, SFINAE, if constexpr, or another technique to conditionally include the constructor and member variable based on whether the template parameter is provided?
Yes — you can expose one SFRegT API that supports both a compile-time address (template parameter) and a runtime address (constructor + member) without duplicating the public member functions. Use either partial specialization + CRTP or a conditional-storage base (std::conditional_t / [[no_unique_address]]) plus SFINAE to enable the runtime constructor; use if constexpr only inside function bodies (it can’t add/remove data members). These patterns let you keep a single set of member functions while the storage/constructor is chosen by template parameter.
Contents
- Problem summary and goals
- SFINAE, if constexpr and partial template specialization: language constraints
- Recommended pattern — conditional storage + SFINAE + if constexpr
- Alternative: explicit specialization + CRTP (no duplication)
- Example usage and typedefs
- Trade-offs, compiler notes and testing
- Sources
- Conclusion
Problem summary and goals
You want one API: the same member functions (Reset, Set, Get, operator(), setBit, …) exposed for two variants:
- A compile-time variant: template<uintptr_t Addr> SFRegT
with no ctor and no per-object storage. - A runtime variant: non-templated-looking SFRegT (e.g. SFRegT<>) with a constexpr constructor that stores the address in a member.
Constraints: avoid duplicating the function implementations and preserve zero-cost for the compile-time case.
SFINAE, if constexpr and partial template specialization: language constraints
Short answers about the language features:
- Templates are instantiated at compile time; you can’t “instantiate a template at runtime” in standard C++ — templates are a compile-time code-generation mechanism (StackOverflow discussion, SoftwareEngineering answer).
- Partial (class) template specialization can change class layout (members and functions) and is allowed for class templates but not function templates (cppreference partial specialization, LearnCpp explanation).
- SFINAE (or requires/constraints) can enable/disable constructors or member functions but cannot remove/add data members by itself — use it to control overloads (cppreference SFINAE).
- if constexpr is evaluated inside function bodies; it cannot be used at class scope to add/remove members. For constexpr constructor rules see cppreference constexpr.
So: combine class-level selection (specialization or conditional base/storage) with SFINAE/constraints for constructors; use if constexpr inside functions only if you need branch-time decisions.
Recommended pattern — conditional storage + SFINAE + if constexpr (no duplication)
This is my recommended approach for clarity and zero-cost for the compile-time case: pick a sentinel value (e.g. DYNAMIC), select one of two storage types with std::conditional_t, and expose one set of member functions that always call a uniform storage.getAddr(). Use SFINAE (or requires) to provide a runtime constructor only for the dynamic case. Mark the (empty) compile-time storage with [[no_unique_address]] so it costs nothing.
C++17-compatible example:
#include <cstdint>
#include <type_traits>
constexpr uintptr_t DYNAMIC = static_cast<uintptr_t>(-1);
// compile-time storage: no data members
template<uintptr_t Addr>
struct CompileTimeStorage {
static constexpr uintptr_t getAddr() noexcept { return Addr; }
};
// runtime storage: holds the address
struct RuntimeStorage {
uintptr_t addr;
constexpr RuntimeStorage(uintptr_t a) noexcept : addr(a) {}
constexpr uintptr_t getAddr() const noexcept { return addr; }
};
template<uintptr_t Addr = DYNAMIC>
class SFRegT {
using Storage = std::conditional_t<Addr == DYNAMIC, RuntimeStorage, CompileTimeStorage<Addr>>;
// allow empty storage to be optimized away
[[no_unique_address]] Storage storage_;
public:
// constructor present only for the runtime variant
template<uintptr_t A = Addr, typename = std::enable_if_t<(A == DYNAMIC)>>
constexpr SFRegT(uintptr_t runtimeAddr) noexcept : storage_(runtimeAddr) {}
void Reset() const noexcept {
auto a = storage_.getAddr();
*reinterpret_cast<volatile uint32_t*>(a) = 0u;
}
void Set(uint32_t value) const noexcept {
auto a = storage_.getAddr();
*reinterpret_cast<volatile uint32_t*>(a) = value;
}
uint32_t Get() const noexcept {
auto a = storage_.getAddr();
return *reinterpret_cast<volatile uint32_t*>(a);
}
uint32_t operator()() const noexcept { return Get(); }
void setBit(uint16_t bitPos) const noexcept {
setSFRBit(storage_.getAddr(), bitPos); // user-provided helper
}
};
Why this works cleanly:
- The compile-time instantiation SFRegT<0x4000’8000> uses CompileTimeStorage
which is empty; [[no_unique_address]] + EBO means no extra per-object storage. - The runtime variant SFRegT<> (Addr defaults to DYNAMIC) has RuntimeStorage and the constexpr ctor is enabled by SFINAE.
- All member functions use the same code (call storage_.getAddr()), so no duplication.
- No reliance on if constexpr at class scope; if constexpr can still be used inside functions when needed.
Related practical pattern references: conditional-members / EBO pattern (Barry’s blog), and embedded register abstractions that favor type-encoded addresses (typesafe register access article).
Alternative: explicit specialization + CRTP (no duplication)
If you prefer partial specialization, you can explicitly specialize SFRegT
Pattern sketch:
// sentinel
constexpr uintptr_t DYNAMIC = static_cast<uintptr_t>(-1);
// CRTP ops implementing the member functions; Derived must provide addr() const
template<typename Derived>
struct SFRegOps {
void Reset() const noexcept {
auto a = static_cast<const Derived*>(this)->addr();
*reinterpret_cast<volatile uint32_t*>(a) = 0u;
}
// other members: Set, Get, setBit, operator()
};
// compile-time variant (no storage)
template<uintptr_t Addr>
struct SFRegT : SFRegOps<SFRegT<Addr>> {
static constexpr uintptr_t addr() noexcept { return Addr; }
};
// runtime variant (specialization holds storage + ctor)
template<>
struct SFRegT<DYNAMIC> : SFRegOps<SFRegT<DYNAMIC>> {
uintptr_t addr_;
constexpr SFRegT(uintptr_t a) noexcept : addr_(a) {}
constexpr uintptr_t addr() const noexcept { return addr_; }
};
Pros and cons:
- Pros: very explicit; CRTP keeps one implementation of functions; full control of class layout via explicit specialization. Partial specialization is allowed because these are class templates (cppreference partial specialization).
- Cons: a specialization must be written manually; some projects prefer the conditional-storage style for less boilerplate.
Example usage and typedefs
Usage examples and convenient aliases:
// compile-time register:
constexpr SFRegT<0x40008000> regCT{}; // no runtime storage, no ctor required
regCT.Reset();
// runtime register:
SFRegT<> regRT(0x40008000); // SFRegT<> defaults Addr=DYNAMIC
regRT.Set(0x1234);
// alias for runtime type
using SFReg = SFRegT<>; // non-template-looking alias
constexpr SFReg runtimeConstexpr(0x40008000); // if you need a constexpr-init object
Notes:
- Use a sentinel value (DYNAMIC) that won’t collide with real addresses.
- If you want a named runtime-only type, use
using SFReg = SFRegT<>;.
Trade-offs, compiler notes and testing
- Size and optimization: conditional-storage + EBO gives zero per-object overhead for the compile-time instantiation. Both patterns allow the compiler to optimize away address computations in compile-time cases. For embedded code, prefer the compile-time encoding when you can — it’s the same approach used by embedded libraries (electronic design paper).
- Language level: the conditional-storage + SFINAE pattern above is C++17-friendly. If you want to use C++20 you can replace enable_if with
requiresorif constexprin places where appropriate, and you can use std::is_constant_evaluated for some runtime/compile-time checks. See cppreference constexpr. - Portability: don’t rely on compiler extensions like __builtin_constant_p unless you need non-standard behavior (StackOverflow example). Some compilers have quirks about what counts as a constant expression for template args — test on your target toolchain (AVR GCC example).
- SFINAE vs specialization vs wrapper: SFINAE is best for enabling/disabling constructors and overloads; partial specialization is the natural way to change layout; a non-templated wrapper that dispatches to a templated implementation is another option if you want a single non-template type name for all runtime use.
- Testing: write small compile-time static_assert checks for the compile-time variant and run-size checks (sizeof) to ensure no unexpected storage in the compile-time variant. Also inspect generated assembly to confirm direct constant loads for compile-time addresses.
Sources
- Partial template specialization — cppreference
- SFINAE — cppreference
- constexpr specifier — cppreference
- https://stackoverflow.com/questions/5979723/do-templates-actually-have-to-be-compile-time-constructs
- https://softwareengineering.stackexchange.com/questions/440796/is-it-possible-to-instantiate-a-template-class-at-runtime
- https://www.learncpp.com/cpp-tutorial/partial-template-specialization/
- https://brevzin.github.io/c++/2021/11/21/conditional-members/
- https://stackoverflow.com/questions/28604580/registers-as-template-parameter
- https://blog.salkinium.com/typesafe-register-access-in-c++/
- https://base.imgix.net/files/base/ebm/electronicdesign/document/2019/03/electronicdesign_10386_anevolutionofspecialfunctionregisterabstraction.pdf
- https://stackoverflow.com/questions/43709441/avr-gcc-compile-error-using-sfr-io-addr-as-a-constant-expression
- https://stackoverflow.com/questions/66995528/conditionally-instantiate-member-variable-on-presence-of-constexpr-definition
Conclusion
You can implement one SFRegT API that supports both compile-time and runtime addresses by choosing storage at the class level (partial specialization or std::conditional_t) and using SFINAE/constraints to expose the runtime constructor only for the dynamic case; use if constexpr only inside functions for behavior branching. The two practical, low-duplication patterns are (1) conditional-storage + SFINAE (recommended) and (2) explicit-specialization + CRTP; both preserve zero-cost compile-time variants and a single public API.