0

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:

  • Should I be worried or at all hesitant about using any of the tricks above to get the program to compile?
  • Is there a better work around to getting reinterpret_cast to work within a constant expression?
  • Just because 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.

Ryoku
  • 397
  • 2
  • 16
  • All those fancy tricks allowed you to **mark** a function as `constexpr`. But did you check if you can actually call it at compile-time? I bet not. – HolyBlackCat Jul 10 '21 at 12:09
  • @HolyBlackCat the value of the `num` variable cannot be assessed at compile time as the `membersArray` is not constant. What should be able to be evaluated at compile time is a reference or pointer to the `num` variable within the `membersArray` which is what I am returning. Is there an easy way to check whether or not this is truly evaluated at compile-time? – Ryoku Jul 10 '21 at 12:16
  • @HolyBlackCat the `membersArray` which is being acted on is static and none of the calls are referencing an instantiated object. If I was to make `Attributes` static, what would that change? – Ryoku Jul 10 '21 at 12:37
  • Sorry, didn't notice that. Then yes, it shouldn't change anything. I'll post a proper answer in a moment. – HolyBlackCat Jul 10 '21 at 12:45
  • Don't write obscur code. Avoid using `reinterpret_cast` as much as possible. `SaveNum` should simply be `num = newTest`. – Phil1970 Jul 10 '21 at 14:40
  • @Phil1970 I am using the `subclass` as a sort of blueprint for how to interpret the `membersArray` data. This way I can reuse the stack allocated `membersArray` for various different kinds of `subClass`'s. That way if the the type of `subClass` being used changes dynamically during the programs lifetime I can overwrite the `membersArray` using the new `subClass`'s blueprint. – Ryoku Jul 11 '21 at 05:17
  • 1
    @Ryoku Then why not use `std::variant`: https://en.cppreference.com/w/cpp/utility/variant? – Phil1970 Jul 12 '21 at 00:55
  • @Phil1970 excellent suggestion considering I was basically programming a low level version of exactly that. To be honest I never even heard of `std::variant` and only had minimal experience with `union`s so this gave me a great opportunity to learn more about how to use those and how not to reinvent the wheel! – Ryoku Jul 17 '21 at 19:18

1 Answers1

1

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 constexprness 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:

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Thanks. One of the things I am curious about is that given the nature of `reinterpret_cast`, would there even be a possible benefit of placing the `constexpr` tag on the `loadNum` function? Theoretically `reinterpret_cast` isn't actually doing anything to the data, just essentially telling the compiler that we will be treating it as a new data type for the time being and offsetof is just a macro that should be resolved at compile time. My intuition says that the address of the location of the `num` data within the array on the stack is already known at compile time and used in the machine code – Ryoku Jul 10 '21 at 13:04
  • @Ryoku Yes, it might be computed at compile-time if the optimizer is smart enough, but again, adding `constexpr` doesn't do anything (except making your code ill-formed NDR because of the tricks). I tried to explain why above. – HolyBlackCat Jul 10 '21 at 13:18