Contra(T), Sink(T), Init(T) — Contravariant Output Parameters

The Core Idea: Contravariance

When a function writes into a parameter (output), the type safety rules run backwards compared to input parameters. This is contravariance.

For inputs, covariance applies: a function accepting Base* can accept Derived* — passing something more specific where something less specific is expected is safe.

For outputs, the reverse is true. A function that writes Base bits into a slot must be given a Base-typed slot. Giving it a Derived-typed slot is unsafe: the writer knows only Base’s layout, and may not fill the fields that Derived adds.

Input  (covariant):     Derived* → Base*   ✓  more specific is acceptable
Output (contravariant): Derived* → Sink(Base)  ✗  more specific is REJECTED

This reversal is what Sink(T), Init(T), and Contra(T) enforce in C++ builds.

The Three Wrappers: A Gradient

Type Use case Corruption? Cost
Contra(T) Read-only pass-through of a contravariant pointer Never Zero
Sink(T) Write-only output; reader must not look at old value Yes, deferred Small
Init(T) Full initialization; old value is always overwritten No (see below) Zero

In C, all three expand to T*. The C++ wrappers enforce contravariance and optionally scramble memory in debug builds.

Contra(T) — Lightweight Contravariant Pass-Through

Use Contra(T) when a function receives an output location and passes it along to another function without writing to it directly. It checks the type direction but does no corruption.

void dispatch(Contra(Base) out);   // just forwarding — no direct write

Sink(T) — Write-Only Output with Corruption

Use Sink(T) to mark output parameters where the caller must not rely on the old contents. When NEEDFUL_DOES_CORRUPTIONS is enabled, the SinkWrapper scrambles the pointed-to memory with 0xBD bytes at the moment the pointer is first used — so any code that reads from the output slot before writing will see garbage and crash loudly.

The corruption is deferred to first use (not on construction) to avoid corrupting another argument’s value before it is evaluated:

// If corruption happened at construction, `ptr` would be corrupt
// before being evaluated as the second argument:
if (some_function(&ptr, ptr)) { ... }
struct Base { int bits; };

void Write_Base(Sink(Base) out) {
    Base* p = out;   // corruption fires here
    p->bits = 99;
}

Init(T) — Full Initialization

Use Init(T) for functions that promise to fill every field of the output. Init is semantically identical to Sink except it skips corruption — there’s no point scrambling bytes that will all be overwritten anyway. This gives Init zero overhead even with NEEDFUL_DOES_CORRUPTIONS on.

To enable corruption for Init as well (for intensive debugging sessions):

#define NEEDFUL_INIT_CORRUPTS_LIKE_SINK  1

The Type Rules

For Sink(T), the accepted pointer types are:

  • T* — identity (always safe)
  • U* where U is a same-layout base class (supertype) of T — meaning T derives from U and sizeof(U) == sizeof(T)

That second case is the surprising one. If Derived : Base {} with no extra fields, you can call Sink(Derived) with a Base*. The function will write a full Derived-worth of bits into the base-typed slot, and because the layouts are identical, this is safe.

The rejected case is the intuitive-but-wrong one: Derived*Sink(Base). Even with same sizeof, Base is more general than Derived. A function that promises to write Base bits does not know about any Derived invariants, and the caller’s Derived object should not be handed off as though it were a plain Base output target.

In other words, the direction of the relationship is flipped relative to input parameters:

Input  (covariant):    Derived* → Base*        ✓  more specific in
Output (contravariant): Base*   → Sink(Derived) ✓  more general in

Exact(T) is the escape hatch when you want precisely T* with no covariance or contravariance at all.

Why Standard-Layout Matters

Needful’s contravariance is grounded on C struct inheritance: all types in a hierarchy must be standard_layout and have the same sizeof. This is the only guarantee that makes a bit-for-bit write into a Base slot safe when the underlying memory is allocated as a Derived object with no extra fields.

The C++ build checks these properties via static_assert inside IsSameLayoutBase.

Indirect Encodings

Some types store an encoding pointer (getter/setter) rather than a plain value. Writing blindly into such a slot corrupts the encoding. Needful’s MayUseIndirectEncoding trait marks these types, and IsContravariant refuses them as Sink/Init targets unless they are themselves wrapped — where the wrapper guarantees safe writes.


Compile-Time Tests

Sink(Derived) accepts Base* — the contravariant direction

A base-typed pointer can be passed where a more-specific Sink is expected, because the writer fills exactly the same bytes (same-sizeof inheritance).

#define NEEDFUL_CPP_ENHANCED  1
#include <cassert>
#include "needful.h"

struct Base    { int bits; };
struct Derived : Base {};  // no extra fields — same layout

static_assert(sizeof(Base) == sizeof(Derived), "same layout required");

void init_derived(Sink(Derived) out) {
    Derived* p = out;
    p->bits = 99;
}

int main() {
    Base b = {};
    init_derived(&b);  // Base* accepted: base is a supertype of Derived
    assert(b.bits == 99);
    return 0;
}

Sink(Base) rejects Derived* — wrong direction, even with same layout

Even though Derived adds no fields (same sizeof), it is a subtype of Base, not a supertype. Passing it as Sink(Base) is rejected.

// MATCH-ERROR-TEXT: could not convert                    <- GCC
// MATCH-ERROR-TEXT: no matching function                 <- GCC alternate
// MATCH-ERROR-TEXT: no instance of overloaded function   <- MSVC
// MATCH-ERROR-TEXT: no viable constructor                <- Clang
#define NEEDFUL_CPP_ENHANCED  1
#include <cassert>
#include "needful.h"

struct Base    { int bits; };
struct Derived : Base {};

static_assert(sizeof(Base) == sizeof(Derived), "same layout");

void write_base(Sink(Base) out) {
    Base* p = out;
    p->bits = 1;
}

int main() {
    Derived d = {};
    write_base(&d);  // ERROR: Sink(Base) must not accept Derived*
    return 0;
}