“constexpr” function is not “const”

This is just a word of caution. C++14 will not be backwards compatible with C++11 in one aspect of constexpr functions.

In C++11, when you define a constexpr member function, it implicitly obtains a const qualifier:

// C++11
struct NonNegative
{
  int i;
  constexpr int const& get() /*const*/ { return i; }
  int& get() { return i; }
};

The first declaration of function get obtains a const qualifier even though we did not type it. Therefore, the two declarations declare two function overloads: a const and a non-const one.

In C++14 this will no longer be the case: the two declarations will define the same non-const member function with two different return types: this will cause a compilation error. If you have already started playing with constexpr functions and you rely on this implicit const qualifier, I suggest that you start adding it explicitly, so that your code still compiles when you switch to C++14 compilers.

What’s wrong with implicit const?

The problem starts when you try to use our type like this:

// C++11
constexpr int i = NonNegative{2}.get(); // ERROR

The (somewhat unusual) rules of C++ dictate that when picking a member function on a temporary class object, the non-const variant is a viable candidate, and in fact preferred to the const one. Our selected non-const member function get is not constexpr, so it cannot be used to initialize a constexpr variable and we get a compile-time error. We cannot fix it by making the non-const overload constexpr, because this would imply the const qualifier…

I said that the rules of best function matching are unusual, because they are quite the opposite when we are selecting the non-member function for a temporary object. In that case we choose const lvalue reference variant, and the to non-const one is not even a viable match:

int const& fget(NonNegative const& n) { return n.i; }
int& fget(NonNegative& n) { return n.i; }

int i = fget(NonNegative{2});

In the case above we select the first overload: a reference to non-const lvalue from the second overload cannot be bound to a temporary.

The above ‘irregularity’ is a problem related to how member functions are matched, and not directly related to constexpr functionality. However, there is another irregularity added along constexpr to C++11. If we define non-member constexpr getters, no implicit const is added:

// C++11
constexpr int const& get(NonNegative const& n) { return n.i; }
constexpr int& get(NonNegative& n) { return n.i; }

NonNegative N = readValue();
constexpr int * P = &get(N); // non-const overload

int main() 
{ 
  *P = 1;
}

Observe what happens. The global N is not a constant. Therefore the second, non-const overload is picked in line 6. But the non-const overload is still a constexpr function! Because rule “constexpr implies const” only applies to the implied this argument of non-static member functions. A constexpr function can take a reference to non-const object and return a reference to its non-const sub-object. This is all fine: we do not know the value of N, but by only passing references around and not reading the value we are not doing anything impossible. Next, we take the address to N’s sub-object. This is still fine: the address of a global is constant — known at compile-time. However, the value at the address P is not constant and can be later assigned to.

If the above example looks too esoteric, consider the following, a bit more realistic one:

constexpr NonNegative* address(NonNegative& n) { return &n; } 

NonNegative n{0}; // non-const
constexpr NonNegative* p = address(n);

This works fine. But if you try to make address a member function, it will stop working:

// C++11
struct NonNegative
{
  // ...
  constexpr NonNegative* maddress() { return this; } // ERROR
};

NonNegative n{0}; // non-const
constexpr NonNegative* p = n.maddress();

This is because maddress is implicitly const, this has type NonNegative const* and cannot be converted to NonNegative*.

Note that it is not a member function that is really const, but the implicit (this) function argument. The declaration of the member function could be depicted in a pseudo language as:

// PSEUDO CODE
struct NonNegative
{
  // ...
  constexpr NonNegative* maddress(NonNegative const& (*this)); 
};

And this implicit function argument, unlike any other function argument, obtains a (sometimes unwanted) const qualifier.

This asymmetry will be removed in C++14. If you want a const qualifier for the implicit argument (this), you need to type it yourself. The following is going to be valid in C++14:

// C++14
struct NonNegative
{
  int i;
  constexpr int const& get() const { return i; }
  constexpr int& get() { return i; }
};

Further reading

  1. Richard Smith, “constexprmember functions and implicit const.”
  2. N3690 — “Programming Languages — C++,” especially § C.3.1.
This entry was posted in programming and tagged , . Bookmark the permalink.

10 Responses to “constexpr” function is not “const”

  1. ethouris says:

    For me, these “somewhat unusual” rules are one of the biggest mistake in C++. It shouldn’t have been ever allowed that temporary objects are modifiable. If you regard how Object().method() is being called, you can quickly realize that inside this Object::method(), *this is actually a mutable lvalue, which means that because of this call it actually turns a rvalue into a modifiable lvalue (imagine: Object& ref() {return *this;} and then showme(Object().ref()) to overcome impossibility to pass temporary by reference). Of course, if temporary objects weren’t modifiable, it would be hard to use expressions like “–end()”, but I don’t think it would be really harmful. On the other hand, when there’s no rule of having temporary objects ALWAYS constant, it suppresses some compiler optimizations: the temporary object of a class type (unlike temporary object of builtin type) must always be an object and have its contents allocated in memory, even if it’s in particular case not necessary.

    Similar thing is when returning a value by function: const makes a difference, but only for class types.

    This mistake has also caused that the expression of using assignment to a temporary object can be only detected by the compiler for builtin types (which is a part of detection of mistakenly used = instead of ==). For class types it’s impossible because the compiler just calls a mutable method on an object, and mutable methods can be called on a temporary object.

  2. Vyacheslav Grigoriev says:

    Good article!

    But the text “I said that the rules of best function matching are unusual, because they are quite the opposite when we are picking the best non-member function for a temporary object. In that case we prefer const lvalue reference to non-const one” is not related to the sample below. First of all the word “prefer” strictly is incorrect related to temporary objects. We _can’t_ bind them to non-const lvalue reference. Secondly after reading this text about binding to const reference I see at the example where N is bind to non-const reference. And the third. “However, the value at the address P is not constant and can be later assigned to. ” – what’s wrong there?

    All other parts of the article clearly explain why constexpr and const for members are ortogonal topics. Thanks!

  3. Pingback: What changes introduced in C++14 can potentially break a program written in C++11? - Tutorial Guruji

  4. Pingback: What changes introduced in C++14 can potentially break a program written in C++11? - PhotoLens

  5. Pingback: C++ 中复杂却很有意思的SFINAE技术 - luozhiyun`s Blog

  6. Pingback: What changes introduced in C++14 can potentially break a program written in C++11? – Row Coding

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.