Repository navigation
[BUG] constexpr without == #761
Description
Title: constexpr without ==.
Description:
In reading P2996R0 Reflection for C++26 2.3 List of Types to List of Sizes,
I couldn't help but wonder,
how would we declare an uninitialized constexpr variable in Cpp2.
This isn't a thing in Cpp1, yet.
But I think it's been mentioned as a possible relaxation to constexpr,
including modifiable constexpr globals (per TU?).
But there's actually a context that already affects Cpp2.
An @interface with a constexpr function (https://compiler-explorer.com/z/aro9K15b1):
struct X {
constexpr virtual char f() const = 0;
};
struct Y : X {
constexpr char f() const override { return 'Y'; }
};
constexpr char call_f(const X* x) { return x->f(); }
static_assert([y=Y{}] { return call_f(&y); }() == 'Y');In Cpp2, the best approximation is (https://cpp2.godbolt.org/z/7Gha4YdK7):
X: @interface type = {
f: (this) -> char;
}
Y: type = {
this: X = ();
operator=: (out this) == { }
f: (override this) -> char == 'Y';
}
call_f: (x: * const X) -> char == x*.f();
main: () = {
static_assert(:() call_f(Y()$&);() == 'Y');
}
// Hack for `constexpr`.
namespace cpp2 {
constexpr auto assert_not_null(auto&& p CPP2_SOURCE_LOCATION_PARAM_WITH_DEFAULT) -> decltype(auto) requires true
{
return CPP2_FORWARD(p);
}
}
But that errors with:
main.cpp2:11:17: error: static assertion expression is not an integral constant expression
11 | static_assert([_0 = Y()]() -> auto { return call_f(&_0); }() == 'Y');
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.cpp2:11:17: note: non-literal type '(lambda at main.cpp2:11:17)' cannot be used in a constant expression
1 error generated.
It's not possible for X to declare a constexpr default constructor or f.
We could attempt using @polymorphic_base.
In Cpp1 (https://compiler-explorer.com/z/Y653WPM7j):
struct X {
constexpr virtual char f() const { return 'X'; }
};
struct Y : X {
constexpr char f() const override { return 'Y'; }
};
constexpr char call_f(const X* x) { return x->f(); }
static_assert([x=X{}] { return call_f(&x); }() == 'X');
static_assert([y=Y{}] { return call_f(&y); }() == 'Y');In Cpp2 (https://cpp2.godbolt.org/z/4MYv5E9vz):
X: @polymorphic_base type = {
operator=: (out this) == { }
operator=: (virtual move this) == { }
f: (virtual this) -> char == 'X';
}
Y: type = {
this: X = ();
operator=: (out this) == { }
f: (override this) -> char == 'Y';
}
call_f: (x: * const X) -> char == x*.f();
main: () = {
static_assert(:() call_f(X()$&);() == 'X');
static_assert(:() call_f(Y()$&);() == 'Y');
}
// Hack for `constexpr`.
namespace cpp2 {
constexpr auto assert_not_null(auto&& p CPP2_SOURCE_LOCATION_PARAM_WITH_DEFAULT) -> decltype(auto) requires true
{
return CPP2_FORWARD(p);
}
}
That's a big jump in the API of X
(a variable can be instantiated,
calling f on isn't a compile-time error,
calling std::terminate() in f helps at compile-time but otherwise delays to runtime).
And it happens to work because the return type is a mere char
(it could be a more complicated data type,
the data-less polymorphic base might need the help of globals).
Minimal reproducer (https://cpp2.godbolt.org/z/7Gha4YdK7):
X: @interface type = {
f: (this) -> char;
}
Y: type = {
this: X = ();
operator=: (out this) == { }
f: (override this) -> char == 'Y';
}
call_f: (x: * const X) -> char == x*.f();
main: () = {
static_assert(:() call_f(Y()$&);() == 'Y');
}
// Hack for `constexpr`.
namespace cpp2 {
constexpr auto assert_not_null(auto&& p CPP2_SOURCE_LOCATION_PARAM_WITH_DEFAULT) -> decltype(auto) requires true
{
return CPP2_FORWARD(p);
}
}
Commands:
cppfront main.cpp2
clang++18 -std=c++23 -stdlib=libc++ -lc++abi -pedantic-errors -Wall -Wextra -Wconversion -Werror=unused-result -I . main.cppExpected result:
Consideration to constexpr without ==.
It might be relevant to some users today.
Cpp1 language evolution might also make it very relevant.
Herb has mentioned limiting global variables to constexpr.
That could make x: int; at namespace scope an uninitialized constexpr variable.
But that wouldn't help with other entities that can be left uninitialized.
Actual result and error:
Cpp2 lowered to Cpp1:
//=== Cpp2 type declarations ====================================================
#include "cpp2util.h"
class X;
class Y;
//=== Cpp2 type definitions and function declarations ===========================
class X {
public: [[nodiscard]] virtual auto f() const -> char = 0;
public: virtual ~X() noexcept;
public: X() = default;
public: X(X const&) = delete; /* No 'that' constructor, suppress copy */
public: auto operator=(X const&) -> void = delete;
};
class Y: public X {
public: constexpr explicit Y();
public: [[nodiscard]] constexpr auto f() const -> char override;
public: Y(Y const&) = delete; /* No 'that' constructor, suppress copy */
public: auto operator=(Y const&) -> void = delete;
};
[[nodiscard]] constexpr auto call_f(X const* x) -> char;
auto main() -> int;
//=== Cpp2 function definitions =================================================
X::~X() noexcept{}
constexpr Y::Y()
: X{ }
{}
[[nodiscard]] constexpr auto Y::f() const -> char { return 'Y'; }
[[nodiscard]] constexpr auto call_f(X const* x) -> char { return CPP2_UFCS_0(f, (*cpp2::assert_not_null(x))); }
auto main() -> int{
static_assert([_0 = Y()]() -> auto { return call_f(&_0); }() == 'Y');
}Output:
main.cpp2:11:17: error: static assertion expression is not an integral constant expression
11 | static_assert([_0 = Y()]() -> auto { return call_f(&_0); }() == 'Y');
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.cpp2:11:17: note: non-literal type '(lambda at main.cpp2:11:17)' cannot be used in a constant expression
1 error generated.
See also:
- Doing away with
=and==: Dropping one extra `=` from declarations to make them less divergent from Cpp1 syntax #742. f: (x) x;is neverconstexpr: Terser function syntax? #714 (reply in thread).- Entities that can be left uninitialized: [SUGGESTION] Add support for defining type aliases. #273 (comment).
Activity
JohelEGP commentedon Oct 19, 2023
This could be considered a sufficiently niche issue for the sake of the general simplicity of Cpp2.
There's similar issues with the mapping of Cpp1 specifiers to Cpp2.
static constexprnon-member objects: b589f5d#commitcomment-129028816.staticnon-member objects: [SUGGESTION] Add support for static variables #212, [SUGGESTION] Static: Keyword with many meanings #322 (https://github.com/hsutter/cppfront/issues?q=is%3Aissue+in%3Atitle+static+is%3Aclosed).staticcall operator for function expressions: P1169R4 3.2.2 Can the static-ness of lambdas be implementation-defined?.The last one is for performance.
Cpp2 has no opt-in and Cppfront doesn't lower to it, so we're backwards-compatible with Cpp1.
If
staticwas the default, we'd lose backwards-compatibility.An explicit
thisparameter (not currently supported) would lower to a generic lambda,and its function pointers are not member function pointers.
The only truly backwards-compatible way would be to explicitly opt-into
static.So, for performance, maybe we'd need something like a
staticvirtual-specifier.JohelEGP commentedon Oct 23, 2023
From https://wg21.link/p2996r0#enum-to-string:
The non-
template forequivalent in Cpp2 is:A way to support the proposed Cpp1
template foris with Cpp2template for.Or, alternatively, if we could specify
easconstexprsomehow.Although, arguably, the
templatekeyword upfront is a good indicator of the loop's nature.Another proposed syntax was
for..., but that doesn't work for reflection.std::meta::members_of(^E)is not a pack, but a range to perform heterogeneous splicing.JohelEGP commentedon Oct 25, 2023
It also seems that
constexprwith==doesn't compose.Only a subset of the built-in metafunctions generate
constexprfunctions.The built-in metafunctions are
structandprintdon't generate functions.constexprfunctions.The ordering metafunctions will be
constexprbecause they lower to=defaulted functions.In commit b589f5d, the enum metafunctions had to stop using
basic_valueto explicitly opt-into the==syntax forconstexpr.Only
interfaceandpolymorphic_baseare really sensitive toconstexpr.You can only use virtual dispatch from a base pointer if its function is
constexpr.The
copyableand value metafunctions never generateconstexprfunctions.Since P2448R2, those can simply be
constexprfunctions.From https://en.cppreference.com/w/cpp/compiler_support:
Or maybe
constexprshould still be considered a contract that requires explicit opt-in.Just like how
operator=works.I've considered a
@constexprtype metafunction to mark all member functions asconstexpr.But that might be too broad (e.g.,
@enum @constexprwould mark streamingoperator<<asconstexpr).I've also thought of giving an argument to
copyableand the value metafunctions to opt-intoconstexpr.JohelEGP commentedon Oct 25, 2023
Maybe
@constexprshould be a variadic meta-metafunction.It would take the metafunctions to invoke and mark the added members as
constexpr.The reflection API would have to be extended to allow declaring never-
constexprmember functions.For example:
.add_runexpr_functionᵖʳᵒᵛⁱᵗⁱᵒⁿᵃˡ foroperator<<.Then
@constexpr(enum)wouldn't declare theoperator<<asconstexpr.@constexpr(basic_value)would declare the functions added bybasic_valueasconstexpr.Then, the enum metafunctions can resume using
basic_value.JohelEGP commentedon Nov 4, 2023
Doesn't seem possible without extending the reflection API.
Metafunction template arguments are just strings.
JohelEGP commentedon Jan 28, 2024
See also #959.