P4276R0
A Design Guideline for Scalar Overloads in std::simd

Published Proposal,

Author:
Audience:
LEWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21

Abstract

std::simd includes scalar overloads for a few operations, but a proposal should not be expected to provide them simply because other recent papers have. 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, such as a distinct hardware instruction or an immediate encoding. This paper proposes a design guideline making that criterion explicit: provide a scalar overload only when there is a demonstrable benefit over broadcasting. We use saturating arithmetic as a worked example of an operation that needs no scalar overload.

1. Introduction

std::simd includes scalar overloads for a few operations where variants of a function or operator accept a scalar where a simd::vec 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 simd::vec together with a scalar, as some other std::simd 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

std::basic_vec 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
simd::vec<int> v = ...;

// resolves to an explicit
// saturating_add(vec, scalar) overload
auto r = std::saturating_add(v, 7);
simd::vec<int> v = ...;

// 7 is implicitly broadcast to vec<int>,
// then the vec/vec form is called
auto r = std::saturating_add(v, 7);

The user writes the same code either way, and it computes the element-wise saturating sum of v and a vector of 7s. There is no separate instruction, no separate semantics, and no observable difference between them. An explicit scalar overload of saturating_add 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 std::simd the implicit conversion in question is the broadcasting constructor, which gives the following design guideline, proposed for adoption by LEWG as guidance for std::simd:

For std::simd 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.

6. Possible polls

Poll: A std::simd 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.

References

Informative References

[P2956R2]
Jan Schultke. Saturation arithmetic for std::simd. URL: https://wg21.link/p2956r2
[P3793R1]
Better shifts for std::simd. URL: https://wg21.link/p3793r1
[P4010]
Funnel shifts for std::simd. URL: https://wg21.link/p4010