1. Introduction
includes scalar overloads for a few operations where variants of a
function or operator accept a scalar where a value would otherwise be
expected. Recent papers continue this theme, with better shifts [P3793R1] and funnel
shifts [P4010R0] adding scalar-operand variants. This has inadvertently 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 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, and that broadcast always produces the correct result. An explicit scalar overload is therefore only useful when it offers a concrete implementation benefit that the broadcast cannot. 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.
2. 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.
3. 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
vectorizable 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 redundant, while 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 can never improve correctness. The only thing it could add is an
implementation benefit.
4. Provide a scalar overload only when there is a benefit
Because broadcasting already produces the correct result, the only justification for a scalar overload is a concrete, demonstrable 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 earns its place by giving the user direct access to the cheaper instruction. The result is identical to broadcasting, with the advantage being better generated code.
The shift, rotate, and funnel-shift [P4010] overloads are not a precedent for "scalar parameter overloads everywhere". Instead, 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 bar. 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.
5. 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 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
6. Possible polls
Poll: A scalar overload should be added only when it offers a
compelling, demonstrable 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.