Skip to content

[BUG] constexpr without == #761

Closed as not planned
Closed as not planned
@JohelEGP

Description

@JohelEGP

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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions