Reputation: 437
To my understanding C++11 specifically designates that reinterpret_cast
cannot be used within a constant expression. The reason (again to my understanding) is that the compiler cannot interpret the validity of the conversion. With that being said, there does seem to be some level of trickery that can be used to allow the function to compile even when using a reinterpret_cast
statement.
I have a situation where a single array of bytes within a parent class can be reinterpreted based on which subclass I want the data to represent at the time.
Within the code I have a constexpr
which returns a reference to the subclasses member variable representation within the array, in this case a uint32_t
variable. Using reinterpret_cast<uint32_t&>()
the code does not compile with the compiler declaring that reinterpret_cast
cannot result in a constant expression. However I can get the code to compile by wrapping the function within a template or by using a trivial ternary expression.
The example code below contains a macro labelled compBranchSwitch
which allows you to quickly switch between compilation scenarios for convenience.
#include <cstdint>
#include <cstddef>
#include <array>
#include <iostream>
#define compBranchSwitch 0 //Switch to determine which branch to compile: 2 - With template function, 1 - With ternary operator, 0 - Without any trickery (should not compile)
struct Attributes {
static std::array<char, 4> membersArray;
struct Subclass {
uint32_t num;
static constexpr uint16_t offsetNum() { return offsetof(Subclass, num); }
#if compBranchSwitch == 2
template<bool nothing> //Unused template parameter that circumvents reinterpret_cast being unusable within a constexpr.
static constexpr uint32_t& LoadNum() { return reinterpret_cast<uint32_t&>(membersArray[offsetNum()]); }
#elif compBranchSwitch == 1
static constexpr uint32_t& LoadNum() { return (true ? reinterpret_cast<uint32_t&>(membersArray[offsetNum()]) : reinterpret_cast<uint32_t&>(membersArray[offsetNum()])); }
#else
static constexpr uint32_t& LoadNum() { return reinterpret_cast<uint32_t&>(membersArray[offsetNum()]); }
#endif
static inline void SaveNum(const uint32_t& newTest) { std::memcpy(&membersArray[offsetNum()], &newTest, sizeof(newTest)); }
};
};
std::array<char, 4> Attributes::membersArray;
void main() {
Attributes::Subclass::SaveNum(32);
#if compBranchSwitch == 2
std::cout << Attributes::Subclass::LoadNum<true>();
#else
std::cout << Attributes::Subclass::LoadNum();
#endif
}
The questions I have are:
reinterpret_cast
to work within a constant expression?reinterpret_cast
is not allowed within a constant expression will the compiler still likely evaluate it at compile time under heavy optimization flags?If it is helpful I am compiling under C++17 and using Visual Studio.
A closely related post on stackoverflow I found helpful for information in regards to the C++11 draft for constant expressions and in discovering the ternary operator trick can be found here.
Upvotes: 0
Views: 1275
Reputation: 96579
Firstly, a compiler can execute a function at compile-time even if it's not constexpr
, as long as it doesn't affect the visible behavior of the program. Conversely it can execute a constexpr
function at runtime, as long as knowing its result is not required at compile-time.
Since you're saying you don't know how to test if your functions are callable at compile-time, it looks to me like you're only adding constexpr
to make your code faster. You don't need to do it, and it probably won't change anything because of what I said above.
As for the tricks you used, they don't do anything useful.
constexpr
doesn't mean that the function can always be executed at compile-time. It means that it can be executed at compile-time for some argument values (function or template arguments).
Example:
constexpr int foo(bool x) // The function compiles.
{
if (x)
return true;
else
return rand();
}
constexpr int a = foo(true); // Ok.
constexpr int b = foo(false); // Error.
int c = foo(false); // Ok.
Compilers are not required to strictly verify that at least one suitable argument exists (because it's impossible in general).
That's what happens in your code. The compiler rejects constexpr
on a function when it's certain that no arguments make it callable at compile-time. When it's uncertain, it lets it slide. But when the function is actually called, it becomes clear that its result is not actually constexpr
, and its constexpr
ness is silently ignored (see example above).
Since there are no possible arguments that allow your functions to be executed at compile-time, your code is ill-formed NDR. NDR ("no diagnostic required") means that compilers are not required to notice this error, and different compilers can be more or less strict about it.
Here's the revelant parts of the standard:
[dcl.constexpr]/6
and/7
6 — For a constexpr function or constexpr constructor that is neither defaulted nor a template, if no argument values exist such that an invocation of the function or constructor could be an evaluated subexpression of a core constant expression, or, for a constructor, an evaluated subexpression of the initialization full-expression of some constant-initialized object ([basic.start.static]), the program is ill-formed, no diagnostic required.
7 — If the instantiated template specialization of a constexpr function template or member function of a class template would fail to satisfy the requirements for a constexpr function, that specialization is still a constexpr function, even though a call to such a function cannot appear in a constant expression. If no specialization of the template would satisfy the requirements for a constexpr function when considered as a non-template function, the template is ill-formed, no diagnostic required.
Or, in simple words:
Upvotes: 1