cppfront
cppfront copied to clipboard
[BUG] `constexpr` without `==`
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.cpp
Expected 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==: #742. f: (x) x;is neverconstexpr: https://github.com/hsutter/cppfront/discussions/714#discussioncomment-7158721.- Entities that can be left uninitialized: https://github.com/hsutter/cppfront/issues/273#issuecomment-1538234710.
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: https://github.com/hsutter/cppfront/commit/b589f5d25e5acdbfd94791e23c0ba0f6fdcd59c6#commitcomment-129028816.staticnon-member objects: #212, #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 static was the default, we'd lose backwards-compatibility.
An explicit this parameter (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 static virtual-specifier.
From https://wg21.link/p2996r0#enum-to-string:
template for (constexpr auto e : std::meta::members_of(^E)) {
The non-template for equivalent in Cpp2 is:
for range do (e) {
A way to support the proposed Cpp1 template for is with Cpp2 template for.
Or, alternatively, if we could specify e as constexpr somehow.
Although, arguably, the template keyword 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.
It also seems that constexpr with == doesn't compose.
Only a subset of the built-in metafunctions generate constexpr functions.
The built-in metafunctions are
interface, polymorphic_base, ordered, weakly_ordered, partially_ordered, copyable, basic_value, value, weakly_ordered_value, partially_ordered_value, struct, enum, flag_enum, union, print
structandprintdon't generate functions.- Only the ordering and enum metafunctions will generate
constexprfunctions.
The ordering metafunctions will be constexpr because they lower to =defaulted functions.
In commit b589f5d25e5acdbfd94791e23c0ba0f6fdcd59c6, the enum metafunctions had to stop using basic_value to explicitly opt-into the == syntax for constexpr.
Only interface and polymorphic_base are really sensitive to constexpr.
You can only use virtual dispatch from a base pointer if its function is constexpr.
The copyable and value metafunctions never generate constexpr functions.
Since P2448R2, those can simply be constexpr functions.
From https://en.cppreference.com/w/cpp/compiler_support:
C++23 feature Paper(s) GCC Clang MSVC Apple Clang Relaxing some constexpr restrictions P2448R2 13 17 (partial)
Or maybe constexpr should still be considered a contract that requires explicit opt-in.
Just like how operator= works.
I've considered a @constexpr type metafunction to mark all member functions as constexpr.
But that might be too broad (e.g., @enum @constexpr would mark streaming operator<< as constexpr).
I've also thought of giving an argument to copyable and the value metafunctions to opt-into constexpr.
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 tocopyableand the value metafunctions to opt-intoconstexpr.
Maybe @constexpr should 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-constexpr member functions.
For example:
- The enum metafunctions would switch to using
.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 usingbasic_value.
Maybe
@constexprshould be a variadic meta-metafunction.
Doesn't seem possible without extending the reflection API. Metafunction template arguments are just strings.
See also #959.