1. Revision History
R0 => R1
-
Added semantic benefit as a reason to provide a scalar overload, with
being a motiviating example which requires a scalar overload to preserve the intended element-wise operation.std :: complex < T > + T
2. Introduction
includes scalar overloads for a few operations: variants of a
function or operator that accept a scalar where a value would otherwise be
expected. Recent papers continue this theme, with better shifts [P3793R1] and funnel
shifts [P4010] adding scalar-operand variants. This has created an expectation that
every new operation should provide scalar overloads too, but they are not needed by
default. In each case where they exist, they do so for a specific, demonstrable
reason, such as preserving distinct scalar semantics or providing access to a distinct hardware instruction.
The reason scalar overloads are not needed by default is that a scalar supplied where a vector is expected is already broadcast to a vector by the converting constructor. Where this preserves the intended element-wise scalar operation, an explicit scalar overload is only useful when it offers a concrete implementation benefit that the broadcast cannot. Where broadcasting does not preserve the intended element-wise scalar operation, preserving that operation is itself a concrete benefit. This paper proposes a design guideline that a scalar overload should only be provided when there is such a benefit, and otherwise rely on the broadcast that the library already performs.
This question came up doing LWG review of saturating arithmetic for SIMD [P2956R2], but does not propose any changes to that paper.
3. Background
The question this guideline answers came up concretely during review of the
saturating-arithmetic paper [P2956R2], where it was questioned whether the saturating operations should accept a together with a scalar, as some other operations do. The question
applies far beyond that one paper since it recurs whenever a new data-parallel
operation is proposed, so it is worth answering once, in general terms, rather
than per paper. This paper does so, and does not propose any change to [P2956R2],
which has been reviewed and approved by LWG.
4. std :: simd already broadcasts scalars
has a converting constructor from a single value ([simd.ctor]),
which allows a scalar argument to be broadcast to every element of the vector. As a
direct consequence, an operation defined only for vector operands already accepts a
scalar in any argument position that participates in conversion, because the scalar
is broadcast to a vector first.
For saturating arithmetic the call a user would write already works today, because the scalar is broadcast implicitly by the converting constructor. A dedicated scalar overload would not change what the user writes, nor the result:
| With a hypothetical scalar overload | Today, via the converting constructor |
|---|---|
|
|
The user writes the same code either way, and it computes the element-wise
saturating sum of and a vector of s. There is no separate instruction, no
separate semantics, and no observable difference between them. An explicit scalar
overload of would therefore be pure redundancy, forcing more API
surface, more wording, and more tests for no behavioural benefit. This
identical-result property is the core of the argument. For an operation whose scalar
form is defined as a broadcast, the result is the same by construction, so a scalar
overload cannot improve correctness. The only thing it could add is an
implementation benefit.
This is not true when the element type has a distinct heterogeneous scalar
operation. For example, arithmetic between and is not in
general the same floating-point operation as first converting the to and then performing complex-complex arithmetic. In that case,
broadcasting changes the element-wise operation, so preserving the heterogeneous
operation is a semantic benefit of an explicit scalar overload.
5. Provide a scalar overload only when there is a benefit
Where broadcasting produces the same result, the justification for a scalar overload is a concrete, demonstrable implementation benefit that the broadcast form cannot achieve, almost always a generated-code benefit. The shift and rotate overloads are the canonical example. A shift by a scalar (or compile-time-constant) count can lower to a shift-by-scalar or shift-by-immediate instruction, whereas a shift by a vector count lowers to a per-element variable-shift instruction. These are genuinely different operations with different cost, so the scalar overload gives the user direct access to the cheaper instruction.
The shift, rotate, and funnel-shift [P4010] overloads are not a precedent for "scalars everywhere", they are a precedent for the principle that a scalar overload is added when, and only when, it does something the broadcast cannot. Uniformity or convenience alone is not sufficient.
Saturating arithmetic does not meet this requirement. There is no distinct "saturating-add-by-scalar" instruction to reach; the scalar form is defined as the element-wise saturating operation against the broadcast scalar, identical to the all-vector form, and broadcasting is always well-formed. The scalar overload would be redundant with the broadcasting constructor, so it should not be added.
6. Proposed guideline
The underlying principle is general: an overload that merely reproduces what an
implicit conversion already does should be added only when it offers a distinct,
demonstrable benefit over relying on that conversion. In the
implicit conversion in question is the broadcasting constructor, which gives the
following design guideline, proposed for adoption by LEWG as guidance for :
For
functions and operators, a scalar overload (one taking a scalar where a vector operand is otherwise expected) should be provided only when it offers a concrete, demonstrable benefit that the broadcasting converting constructor cannot. Such a benefit can be semantic, because broadcasting does not preserve the intended element-wise scalar operation, or implementational, such as access to a distinct hardware instruction or immediate encoding. Such a benefit should be compelling, and uniformity or convenience alone is not sufficient. Where a scalar argument would merely be broadcast to a vector and produce an identical result, no scalar overload should be added since the broadcasting constructor already provides the functionality.std :: simd
7. Possible polls
Poll: A scalar overload should be added only when it offers a
compelling, demonstrable semantic or implementation benefit over broadcasting via the converting
constructor.
Poll: For [P2956R2] there is no reason to delay its inclusion in the C++29 working draft for lacking scalar overloads.