Internals: The Template Cast Operator Problem
This article explains a recurring implementation quirk in Needful’s wrapper types: why certain conversions use the idiom c_cast(T*, c_cast(void*, u)) instead of a direct cast, and what goes wrong if you try the obvious alternatives.
The Setup
Needful’s C++ enhancement mode wraps raw pointers in types like ContraWrapper<T*>, SinkWrapper<T*>, and OptionWrapper<T> to get compile-time enforcement of type constraints.
These wrappers need to convert back to raw pointers frequently — but they live inside template constructors that accept a family of compatible types:
template<typename U, IfContravariant<U, T>* = nullptr>
ContraWrapper(const U& u) {
this->p = c_cast(T*, c_cast(void*, u)); // ← the idiom in question
}
Here U might be Derived*, or SinkWrapper<Derived>, or ContraWrapper<Derived> — anything that IfContravariant has certified is layout-compatible with T*. The job is to get T* out of it.
Why not just write static_cast<T*>(u)?
What Breaks With static_cast<T*>(u) Directly
Case 1: U is a raw pointer (Derived*)
// T = Base, U = Derived*, u is Derived*
static_cast<Base*>(u); // works fine — Derived* -> Base* is standard upcast
This case actually works. static_cast handles upcasts along inheritance hierarchies correctly.
Case 2: U is a wrapper type (SinkWrapper<Derived>)
// T = Base, U = SinkWrapper<Derived>, u is SinkWrapper<Derived>
static_cast<Base*>(u); // does NOT work
SinkWrapper<Derived> has this conversion operator:
operator T*() const { ... } // T is Derived here — yields Derived*
But there is no operator Base*(). So static_cast<Base*>(SinkWrapper<Derived>) finds no conversion and fails.
The wrapper does have a templated explicit conversion:
template<typename U>
explicit operator U*() const {
return const_cast<U*>(reinterpret_cast<const U*>(p));
}
static_cast can invoke explicit conversion operators, so static_cast<Base*>(u) would call explicit operator Base*() — but look what that does: it reinterpret_casts Derived* to const Base* then strips const. reinterpret_cast between pointer types does not adjust for base class offsets. For single inheritance with no added fields (Needful’s requirement for contravariant types) this happens to produce the same address — but it bypasses the language’s semantic guarantees entirely. It’s also wrong in principle and misleading to future readers.
Why Going Through void* Is Correct
The idiom:
this->p = c_cast(T*, c_cast(void*, u));
breaks into two steps:
Step 1: c_cast(void*, u) — C-style cast to void*.
A C-style cast attempts conversions in this order: const_cast, static_cast, static_cast with const, reinterpret_cast, then combinations. Crucially, it also invokes implicit conversion operators. SinkWrapper<Derived> has:
operator Derived*() const { ... } // implicit — no explicit keyword
So (void*)u resolves as:
- Invoke implicit
operator Derived*()→ yieldsDerived*(with correct pointer arithmetic, including any base subobject adjustments) - Convert
Derived*tovoid*— trivially valid
The pointer that comes out is the correct adjusted address.
Step 2: c_cast(T*, result) — C-style cast from void* to Base*.
void* → Base* is a standard conversion. Because step 1 produced the correct Derived* (which for a standard-layout type with no added fields is the same address as Base*), this yields the right Base*.
The two-step round-trip through void* is the idiomatic way to do pointer-type conversions that the type system would otherwise reject, while still going through semantically correct pointer values.
Why c_cast Instead of a Plain C-Style Cast
c_cast expands to a C-style cast (T)(expr). It’s used rather than bare parentheses for two reasons:
-
Visibility. A bare
(void*)uin template code is easy to miss during review.c_cast(void*, u)is a visible signal that something unusual is happening. -
Searchability. Every use of this workaround can be found by searching for
c_cast. If a cleaner solution becomes possible in a later C++ standard, all sites are easy to locate and update.
Which Classes Use This and Why
| Class | Uses the workaround? | Why |
|---|---|---|
ContraWrapper<T*> | Yes | Accepts any contravariant U; U may be a wrapper |
SinkWrapper<T*> | Yes | Same; also needs to convert from other SinkWrappers |
InitWrapper<T*> | Inherits from ContraWrapper | Constructors inherited |
NeedWrapper<T*> | No | Only constructed from T* directly; no wrapper sources |
OptionWrapper<T> | No | Wraps values, not pointer hierarchies |
ResultWrapper<T> | No | Same |
The pattern only arises where a wrapper must accept another wrapper via the contravariance relationship — i.e., wherever the source type is generic and may be a wrapper rather than a raw pointer.
Could This Be Avoided?
A few alternatives were considered:
reinterpret_cast<T*>(u.p) directly — accessing the field directly bypasses the conversion operator entirely, which would be cleaner. But p is a private field accessed via NEEDFUL_DECLARE_WRAPPED_FIELD, and friend declarations for every pair of wrapper types would be required. The c_cast workaround works without coupling the types together.
A .raw_pointer() accessor — adding a public method that returns the raw pointer would work and be clean. This would be a reasonable future improvement. The downside is API surface: it exposes an unsafe extraction path that callers could use incorrectly. The current operator T*() triggers corruption side effects on SinkWrapper, which a plain .p accessor would bypass.
C++23 explicit(false) or deducing this — no new standard feature directly solves this. The fundamental issue is that C++ cannot template the return type of a conversion operator in a way that participates in implicit conversion chains. That’s a fundamental language rule, not a standard-version limitation.
Summary
The c_cast(T*, c_cast(void*, u)) idiom exists because:
- Wrapper types expose
operator T*()for their specificT, not for base types. static_castcannot cross from a wrapper to an unrelated pointer type.- The templated
explicit operator U*()usesreinterpret_cast, which is semantically wrong for inheritance relationships. - Going through
void*forces the implicitoperator T*()to fire first (giving the correct adjusted pointer), then allows a safevoid* → Base*conversion.
This is tracked as an internal implementation detail. If a .raw_pointer() accessor or equivalent is added in the future, all c_cast(T*, c_cast(void*, ...)) sites in needful-contra.hpp are the candidates for cleanup.