Description
Title: operator is
should be constrained or answered statically.
Description:
- Constrained:
operator is
should be non-viable given a type-unsafe input. - Answered statically: See https://quuxplusone.github.io/blog/2022/01/04/test-constexpr-friendliness/.
This is to implement P2392's §4.2.
It's also to correctly resolve #433,
as split off from #491:
Comments moved from #491.
The case of this PR is:
The alternative's is-as-expression-target matches
but the statement is ill-formed for the inspected type.
P2392 doesn't say what should happen in this case.
If one extracted the wording from the design, it would be UB by omission
IIUC what P1371 says, then it would similarly be UB by omission:
When inspect is executed, its condition is evaluated and matched in order (first match semantics) against each
pattern. If a pattern successfully matches the value of the condition and the boolean expression in the guard
evaluates to true (or if there is no guard at all), then the value of the resulting expression is yielded or control is
passed to the compound statement, depending on whether the inspect yields a value. If the guard expression
evaluates to false, control flows to the subsequent pattern.If no pattern matches, none of the expressions or compound statements specified are executed. In that case if
the inspect expression yields void, control is passed to the next statement. If the inspect expression does not
yield void, std::terminate will be called.
It also links to
https://github.com/solodon4/Mach7,
https://github.com/mpark/patterns, and
https://github.com/jbandela/simple_match.
But this corner case doesn't seem to be mentioned in their README.
Originally posted by @JohelEGP in #491 (comment)
How about this instead?
For a dependent inspect
,
when the is-as-expression-target of an alternative is well-formed,
the corresponding statement should also be well-formed.
So we move
from a runtime-checked contract
to a compile-time assertion.
Originally posted by @JohelEGP in #491 (comment)
UB by omission
Actually,
the proposals are clear that when there's a match,
execution continues at the corresponding statement.
The proposals are also clear in that
an ill-formed condition means the whole branch is a discarded statement.
So it falls out from the existing rules
that a well-formed condition implies the corresponding statement is also instantiated.
The actual bug is
that the validity check happens for the statement
and not at the condition (and thus for the whole branch).
Another bug is that an is
-expression is always be well-formed,
resulting in always-false
when the equivalent without is
would be ill-formed.
There is an example at P2392 §3.4.3
that mixes conditions that don't work for all intended types.
It can demonstrate the issue.
Here is a reduction: https://cpp2.godbolt.org/z/c5rsjP6a7.
in: (min, max) -> _ = :<T> (x: T) -> bool requires std::integral<T> = { return min$ <= x <= max$; };
f: <T> (x: T) -> _ = {
return inspect x -> std::string {
is std::string = "a string";
is (in(1, 2)) = "1 or 2";
is _ = "something else";
};
}
main: () = {
s: std::string = ();
std::cout << "(f(s))$\n" // prints "a string".
<< "(f(1))$\n" // prints "1 or 2".
<< "(f(42))$\n" // prints "something else".
<< "(s is (in(1, 2)))$\n"; // prints "0".
}
Notice how s is (in(1, 2))
is well-formed.
It should be ill-formed outside a dependent inspect
.
As the condition of the alternative of a dependent inspect
,
it should make the alternative discarded.
Originally posted by @JohelEGP in #491 (comment)
Consider this degenerate case:
- A debug build.
- An
inspect
of 100 alternatives:- First, 99 alternatives with a condition of a predicate well-formed for integers only,
- then, the match-all
is _
.
Because a condition is always well-formed,
an always-false
condition for a given input type isn't optimized out.
Given an input std::string
,
the first 99 conditions are necessarily evaluated sequentially,
even though they're all always-false
.
Originally posted by @JohelEGP in #491 (comment)
Let's take the same inspect
again.
return inspect x -> std::string {
is std::string = "a string";
is (in(1, 2)) = "1 or 2";
is _ = "something else";
};
For the given conditions,
the operator is
overloads of std::variant<std::string, i32>
should be well-formed and necessarily runtime-evaluated.
The same for std::variant<std::monostate>
should be ill-formed.
So std::variant::operator is
should only work
at runtime and when the query can be forwarded to one of its variant alternatives.
Nothing changes the fact that if the condition is well-formed, the statement should be instantiated.
Originally posted by @JohelEGP in #491 (comment)
Once operator is
is constrained or answered statically,
it becomes necessary to fix inspect
to omit alternatives whose conditions are non-viable.
It also becomes possible
to omit alternatives whose conditions always-false
.
Note that there's a distinction between an operator is
that is constrained and answered statically.
A constrained operator is
has preconditions on the input type.
There are other contexts where we might want the semantics of an inspect
's alternative's condition.
g: <T> (x: T) = {
// Continues working.
if constexpr T is std::string { }
// The following can work given C++23's P2280 (C++20 DR).
if constexpr x is std::string { }
// Will be `true` for `T == int` (using the built-in `operator is`).
// For a `T` that is a specialization of `std::variant`,
// it would be ill-formed,
// but can be made to work
// if given the semantics of the condition of an `inspect`'s alternative
// (i.e., always-`false`).
if constexpr x is 0 { }
// Can be `true` for `T == std::integral_constant<i32, 0>` if overloaded.
}
Minimal reproducer (https://cpp2.godbolt.org/z/87hjsccYd):
in: (min, max) -> _ = :<T> (x: T) -> bool requires std::integral<T> = { return min$ <= x <= max$; };
f: <T> (x: T) -> _ = {
return inspect x -> std::string {
is std::string = :(x) -> _ = { return "a string"; }();
is (in(1, 2)) = "1 or 2";
is _ = "something else";
};
}
main: () = {
s: std::string = ();
std::cout //
<< "(f(s))$\n" // prints ``.
<< "(f(1))$\n" // prints `1 or 2`.
;
std::cout //
<< "(s is (in(1, 2)))$\n" // prints `0`.
;
}
Commands:
cppfront -clean-cpp1 main.cpp2
clang++17 -std=c++23 -stdlib=libc++ -lc++abi -pedantic-errors -Wall -Wextra -Wconversion -I . main.cpp
Expected result:
f(s)
: An error when instantiating:(x) -> _ = { return "a string"; }()
.f(1)
: Alternative withis std::string
to be statically elided.s is (in(1, 2))
: An error, just likein(1, 2)(s)
.
Actual result and error:
f(s)
: Unconditionally returnsstd::string()
because the matched alternative's statement is ill-formed.f(1)
: Unconditionally evaluatesis std::string
that will always befalse
.s is (in(1, 2))
: Unconditionally results infalse
, even thoughin(1, 2)(s)
is ill-formed.
Cpp2 lowered to Cpp1.
#include "cpp2util.h"
[[nodiscard]] auto in(auto const& min, auto const& max) -> auto;
template<typename T> [[nodiscard]] auto f(T const& x) -> auto;
auto main() -> int;
[[nodiscard]] auto in(auto const& min, auto const& max) -> auto { return [_0 = min, _1 = max]<typename T>(T const& x) -> bool
requires (std::integral<T>)
{return [_0 = _0, _1 = x, _2 = _1]{ return cpp2::cmp_less_eq(_0,_1) && cpp2::cmp_less_eq(_1,_2); }(); }; }
template<typename T> [[nodiscard]] auto f(T const& x) -> auto{
return [&] () -> std::string { auto&& __expr = x;
if (cpp2::is<std::string>(__expr)) { if constexpr( requires{[](auto const& x) -> auto{return "a string"; }();} ) if constexpr( std::is_convertible_v<CPP2_TYPEOF(([](auto const& x) -> auto{return "a string"; }())),std::string> ) return [](auto const& x) -> auto{return "a string"; }(); else return std::string{}; else return std::string{}; }
else if (cpp2::is(__expr, (in(1, 2)))) { if constexpr( requires{"1 or 2";} ) if constexpr( std::is_convertible_v<CPP2_TYPEOF(("1 or 2")),std::string> ) return "1 or 2"; else return std::string{}; else return std::string{}; }
else return "something else"; }
();
}
auto main() -> int{
std::string s {};
std::cout //
<< cpp2::to_string(f(s)) + "\n" // prints ``.
<< cpp2::to_string(f(1)) + "\n";// prints `1 or 2`.
std::cout //
<< cpp2::to_string(cpp2::is(std::move(s), (in(1, 2)))) + "\n";// prints `0`.
}
Output.
Program returned: 0
1 or 2
0