Skip to content

fix: let the accessors be called on a temporary - #86

Merged
oberbichler merged 1 commit into
mainfrom
fix/accessors-on-temporaries
Jul 27, 2026
Merged

oberbichler merged 1 commit into
mainfrom
fix/accessors-on-temporaries

Conversation

@oberbichler

Copy link
Copy Markdown
Owner

Problem

The accessors take their object by deducing this, declared as an lvalue reference — which cannot bind to a temporary. So a result could not be read from the expression that produced it:

(a * b).f()        // error: candidate function not viable:
sqrt(a).g(0)       //        expects an lvalue for object argument
a.sin().h(0, 0)

Every C++ caller had to introduce a named variable for every intermediate result. Six accessors were affected: data, ptr, f, g, h(i) and h(i, j).

This came in with the deducing-this refactor of the C++23 modernisation, which replaced the duplicated const/non-const overload pairs. The old pairs had a const overload that bound to temporaries; the single deducing-this version does not.

Fix

The object parameter becomes a forwarding reference — this auto &&self instead of this auto &self — which binds to lvalues, const lvalues and temporaries alike. One character per accessor.

SScalar is unaffected: its f() and d() return by value.

Caveat, and it is documented in the header

As with std::vector::operator[], a reference obtained from a temporary is only valid within the full expression:

const auto &r = (a * a).f();   // dangles, and no compiler diagnoses it

I checked: -Wall -Wextra -Wpedantic emits nothing for that. The note sits where the accessors are declared.

What did not change

The writable path: result.f() = value on an lvalue still works — the factories rely on it — and so does reading through a const lvalue. Verified separately as well as by the suite.

Tests

The property is stated twice over.

Callability, as concepts over all four combinations of order and sizing — 18 assertions, all of which failed before the change:

test_variants.cpp:135: ERROR: CHECK( ReadsValue<T> ) is NOT correct!
test_variants.cpp:136: ERROR: CHECK( ReadsGradient<T> ) is NOT correct!
test_variants.cpp:137: ERROR: CHECK( ReadsData<T> ) is NOT correct!
test_variants.cpp:138: ERROR: CHECK( ReadsPointer<T> ) is NOT correct!
test_variants.cpp:141: ERROR: CHECK( ReadsHessian<T> ) is NOT correct!

Deliberately expressed as concepts rather than as direct calls, so the red state is a failing assertion rather than a build error.

Values, read from temporaries and compared against the expectations the surrounding cases already use — including a chained read, a.sin().exp().f(), which is the case that makes this worth fixing at all.

Verification

  • C++ 106 test cases, 1362 assertions — Debug and Release with -Werror, and under clang with ASan+UBSan
  • Python 239 passed
  • clang-tidy unchanged at 50; clang-format --Werror and ruff format --check clean

Notes

Independent of #84 and #85 — this touches DDScalar's accessors, those two touch SScalar. Based on main, merges in any order.

Found while measuring for the SScalar work: a benchmark of mine would not compile, and the compiler error turned out to be a defect in the library rather than in the benchmark.

The accessors take their object by deducing this, declared as an lvalue
reference, which cannot bind to a temporary. So a result could not be read from
the expression that produced it:

    (a * b).f()          error: candidate function not viable:
    sqrt(a).g(0)                expects an lvalue for object argument
    a.sin().h(0, 0)

Every C++ caller had to introduce a named variable for every intermediate
result. Six accessors were affected: data, ptr, f, g, h(i) and h(i, j). The
object parameter becomes a forwarding reference, which binds to lvalues, const
lvalues and temporaries alike.

As with std::vector::operator[], a reference obtained from a temporary is only
valid within the full expression, and no compiler diagnoses keeping it. That is
noted where the accessors are declared.

The writable path is unchanged: result.f() = value on an lvalue still works,
which the factories rely on, and so does reading through a const lvalue.

Tests state the property twice over. Concepts check that each accessor is
callable on an rvalue, across all four combinations of order and sizing -- 18
assertions, all of which failed before the change. Then the values read from a
temporary are compared against the expectations the surrounding cases already
use, including a chained read, a.sin().exp().f(), which is the case that makes
this worth fixing.

  C++ 106 test cases, 1362 assertions, Debug and Release with -Werror and
  under clang with ASan+UBSan
  Python 239 passed
  clang-tidy unchanged at 50, clang-format and ruff format clean
@oberbichler
oberbichler merged commit 1b33aae into main Jul 27, 2026
17 checks passed
@oberbichler
oberbichler deleted the fix/accessors-on-temporaries branch July 27, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant