llvm / llvm-project

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
http://llvm.org
Other
28.56k stars 11.81k forks source link

clang rejects regular constexpr member function in struct with virtual base class #97266

Open Rush10233 opened 3 months ago

Rush10233 commented 3 months ago

First, the following code is rejected by clang but accepted by other compilers like GCC, MSVC, and ICC:

    struct base { };
    struct derived : virtual base {
        constexpr int tst(){return 1;}
    };

https://godbolt.org/z/rj63nKP4v

Clang reports the following message:

<source>:3:23: error: constexpr member function not allowed in struct with virtual base class
    3 |         constexpr int tst(){return 1;}
      |                       ^
<source>:2:22: note: virtual base class declared here
    2 |     struct derived : virtual base {
      |                      ^~~~~~~~~~~~
1 error generated.

The diagnostic seems not such necessary because member function tst doesn't involve any override and can be specified during compile stage.

It's more interesting that the overloaded copy assignment operator is also accepted by other compilers, which definitely contains the instantiation of the virtual base class:

    struct base { };
    struct derived : virtual base {
        //reject
        //constexpr derived(const derived &)=default;
        //reject
        //constexpr derived(derived &&)=default;
        //accept
        constexpr derived& operator=(derived const&) =default;
    };

https://godbolt.org/z/rYPEYTex8

Copy ctors and move ctors are always rejected though.

MitalAshok commented 3 months ago

For the first one (normal member function)

https://github.com/llvm/llvm-project/blob/1b8ab2f08998d3220e5d95003d47bb3d7cac966b/clang/lib/Sema/SemaDeclCXX.cpp#L1826-L1827

In C++20 (and before), this matches all of the requirements https://timsong-cpp.github.io/cppwp/n4861/dcl.constexpr#3

Notably C++20 had a requirement for constructors/destructors only:

  • if the function is a constructor or destructor, its class shall not have any virtual base classes;

Which is being applied to all member functions by Clang. Stuff like this needs to be accepted: https://godbolt.org/z/76WzG3jv6


For the second one (defaulted copy-assign operator): In C++23 after p2448r2, it should be accepted.

The constructors not being allowed to be constexpr is still not allowed in C++23. https://eel.is/c++draft/dcl.constexpr#3.2

A function is constexpr-suitable if:

  • [...]
  • if the function is a constructor or destructor, its class does not have any virtual base classes. Except for instantiated constexpr functions, non-templated constexpr functions shall be constexpr-suitable.

So constexpr derived(const derived &)=default; is always ill-formed.

I believe gcc shouldn't accept it before C++23. https://timsong-cpp.github.io/cppwp/n4861/dcl.fct.def.default#3.sentence-1 (C++20)

An explicitly-defaulted function that is not defined as deleted may be declared constexpr or consteval only if it is constexpr-compatible ([special]).

tbaederr commented 3 months ago

There is even a FIXME comment about this: https://github.com/llvm/llvm-project/blob/208a08c3b7b00c05629c3f18811aac81f17cd81b/clang/lib/Sema/SemaDeclCXX.cpp#L1820-L1841

llvmbot commented 3 months ago

@llvm/issue-subscribers-clang-frontend

Author: None (Rush10233)

First, the following code is rejected by clang but accepted by other compilers like GCC, MSVC, and ICC: ```c++ struct base { }; struct derived : virtual base { constexpr int tst(){return 1;} }; ``` [https://godbolt.org/z/rj63nKP4v](https://godbolt.org/z/rj63nKP4v) Clang reports the following message: ```c++ <source>:3:23: error: constexpr member function not allowed in struct with virtual base class 3 | constexpr int tst(){return 1;} | ^ <source>:2:22: note: virtual base class declared here 2 | struct derived : virtual base { | ^~~~~~~~~~~~ 1 error generated. ``` The diagnostic seems not such necessary because member function `tst` doesn't involve any override and can be specified during compile stage. It's more interesting that the overloaded copy assignment operator is also accepted by other compilers, which definitely contains the instantiation of the virtual base class: ```c++ struct base { }; struct derived : virtual base { //reject //constexpr derived(const derived &)=default; //reject //constexpr derived(derived &&)=default; //accept constexpr derived& operator=(derived const&) =default; }; ``` [https://godbolt.org/z/rYPEYTex8](https://godbolt.org/z/rYPEYTex8) Copy ctors and move ctors are always rejected though.
frederick-vs-ja commented 3 months ago

In C++20 (and before), this matches all of the requirements https://timsong-cpp.github.io/cppwp/n4861/dcl.constexpr#3

However, in this example it seems that tst can never produce a constant (sub)expression (even in C++23).

As derived is not an implicit-lifetime type and can't have any constexpr constructor due to the virtual base, it is impossible to start lifetime of a derived object in constant evaluation. As a result, a call to the non-static member function tst is not allowed in constant evaluation as any attempt would cause UB, and it's ill-formed, no diagnostic required to mark tst constexpr until C++23.

In C++23, it should be well-formed to make tst constexpr.

MitalAshok commented 3 months ago

@frederick-vs-ja You don't need an object to start its lifetime inside the constant expression to call a member function on it (https://stackoverflow.com/q/32803708)

struct base { };
struct derived : virtual base {
    constexpr int tst() {return 1;}
};
extern derived d;
static_assert(d.tst() == 1);  // Well formed in C++20 (C++11 even I think)
frederick-vs-ja commented 3 months ago

@frederick-vs-ja You don't need an object to start its lifetime inside the constant expression to call a member function on it (https://stackoverflow.com/q/32803708)

struct base { };
struct derived : virtual base {
    constexpr int tst() {return 1;}
};
extern derived d;
static_assert(d.tst() == 1);  // Well formed in C++20 (C++11 even I think)

Yeah, you're right (thanks to P2280R4). Not sure whether #95474 should also address this.

MitalAshok commented 3 months ago

It was valid even before because there are no unknown references (d is a constant glvalue, and there is nothing with reference or pointer type), so this is unrelated. See the second example (auto rando() here: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2280r4.html#other-examples which is only invalid because of the reference type, and would be valid if the parameter was by-value.

zygoloid commented 3 months ago

Wow. Looks like I got this wrong 13 years ago. Oops!