Document Number |
P1064R0 |
Date |
2018-05-04 |
Project |
Programming Language C++ |
Audience |
Evolution Working Group |
Summary |
This paper proposes allowing virtual function calls in constant expressions. |
Summary
Virtual function calls are currently prohibited in constant expressions. Since in a constant expression the dynamic type of the object is required to be known (in order to, for example, diagnose undefined behavior in casts), the restriction is unnecessary and artificial. We propose the restriction be removed.
Proposed Changes
— it shall not be virtual (13.3);
Q&A
Can a constexpr
virtual function override a non-constexpr
one?
Yes. This is required when, for instance, the programmer has no control over the base class, or when the base virtual function is pure.
Can a non-constexpr
virtual function override a constexpr
one?
Yes.
What happens when some overriders are constexpr
and some are not?
The final overrider is selected, as usual, and if it’s not constexpr
, the
expression is not a constant expression.
Motivating Example
The standard library provides, in [syserr], a very well-designed framework for reporting error codes. This framework is not perfect, and can be enhanced in several ways, two of which we’ll use to illustrate the motivation for this proposal.
First, error_code
is not constexpr
-"enabled", and it’s useful for it to be
not just because this would make it usable in constant expressions, but because
constexpr
-"enabled" types tend to optimize better, and error_code
is used
in performance-sensitive contexts and by performance-sensitive audiences.
Adding the necessary constexpr
qualifiers to error_code
and making it a
literal type isn’t hard, as it’s basically a pair of an integer and a pointer.
We end up with
class error_code
{
private:
int val_;
const error_category* cat_;
public:
constexpr error_code() noexcept;
constexpr error_code(int val, const error_category& cat) noexcept;
template<class ErrorCodeEnum>
constexpr error_code(ErrorCodeEnum e) noexcept;
constexpr void assign(int val, const error_category& cat) noexcept;
template<class ErrorCodeEnum>
constexpr error_code& operator=(ErrorCodeEnum e) noexcept;
constexpr void clear() noexcept;
constexpr int value() const noexcept;
constexpr const error_category& category() const noexcept;
constexpr explicit operator bool() const noexcept;
error_condition default_error_condition() const noexcept;
string message() const;
};
A second enhancement we might wish to pursue is to address the limitation of
error_code
of hardcoding zero as the success value. There are error categories
that consider all nonnegative values successful, and there are (admittedly very rare)
others in which zero is a failure. To address this, we might delegate the
responsibility of deciding whether a value represents a success to the error
category, by adding a virtual member function failed
to it:
class error_category
{
public:
// ...
virtual bool failed(int ev) const noexcept;
// ...
};
Then we add a member function failed
to error_code
:
class error_code
{
public:
// ...
bool failed() const noexcept { return cat_->failed(val_); }
// ...
};
Unfortunately, we can’t pursue both of these at the same time. Since error_code::failed
calls the virtual error_category::failed
, it can’t be constexpr
. We are forced to
choose, and there is no inherent reason for this choice being forced on us.
With this proposal, we just declare both failed
functions as constexpr
and everything
sorts itself out.
Implementability
We have produced a fairly trivial patch for Clang that is successfully able to compile the following example:
struct X1
{
virtual int f() const = 0;
};
struct X2: public X1
{
constexpr virtual int f() const { return 2; }
};
struct X3: public X2
{
virtual int f() const { return 3; }
};
struct X4: public X3
{
constexpr virtual int f() const { return 4; }
};
constexpr int (X1::*pf)() const = &X1::f;
constexpr X2 x2;
static_assert( x2.f() == 2 );
static_assert( (x2.*pf)() == 2 );
constexpr X1 const& r2 = x2;
static_assert( r2.f() == 2 );
static_assert( (r2.*pf)() == 2 );
constexpr X1 const* p2 = &x2;
static_assert( p2->f() == 2 );
static_assert( (p2->*pf)() == 2 );
constexpr X4 x4;
static_assert( x4.f() == 4 );
static_assert( (x4.*pf)() == 4 );
constexpr X1 const& r4 = x4;
static_assert( r4.f() == 4 );
static_assert( (r4.*pf)() == 4 );
constexpr X1 const* p4 = &x4;
static_assert( p4->f() == 4 );
static_assert( (p4->*pf)() == 4 );
At the time of writing, our proof of concept does not yet handle virtual calls in constructors and destructors properly, but we expect extending it to be correct in this area to not pose any significant problems.
On the other hand, covariant returns appear to work under our implementation, even though we have made no specific effort to handle them, as evidenced by the following example compiling without errors:
struct X1
{
constexpr virtual X1 const* f() const { return this; }
};
struct Y
{
int m = 0;
};
struct X2: public Y, public X1
{
constexpr virtual X2 const* f() const { return this; }
};
constexpr X1 x1;
static_assert( x1.f() == &x1 );
constexpr X2 x2;
constexpr X1 const& r2 = x2;
static_assert( r2.f() == &r2 );
Further Work
[expr.const] p2 disallows dynamic_cast
and typeid
on polymorphic types.
These restrictions are also unnecessary for the same reason; compilers already
maintain the dynamic type information required to resolve them. It would be
a natural extension of this proposal to eliminate those two restrictions as well.
Acknowledgments
The authors thank Richard Smith for his help.