Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

constexpr-function parameter is considered constexpr if used directly but not if used to call another constexpr-function

Tags:

c++

constexpr

While experimenting with constexpr functions and templates (and non-type template arguments), I stumbled upon a phenomenon, and I cannot understand which rule brings it into effect.

So my question essentially is "Why does this happen", according to the rules about constexpr-s. "this" is the following.

In one of the constexpr functions, if a parameter is used directly then there is no problem with this parameter being used in a compile-time computation. (example lines 2)

When the same parameter is used as an argument to another constexpr-function, then the compiler complaints that this expression (the parameter id) is not a constexpr. (example line 3)

In short:

template <typename T> constexpr std::size size (T obj) { return obj.size(); }
template <typename T> constexpr auto sz1 (T obj) { return std::make_index_sequence< obj.size() > { }.size(); } // OK ...
template <typename T> constexpr auto sz2 (T obj) { return std::make_index_sequence< size(obj) > { }.size(); } // ERROR
  // "obj" is [suddenly] not a constexpr

This happens with both g++-4.9.1 and clang++-3.4.2 .

Below is a small test program for quick and easy experimentation.


#include <utility>
#include <array>
#include <iostream>

// utils
template <size_t N> using require_constexpr = std::make_index_sequence<N>;
template <typename...> constexpr void noop (void) { }

// size() wrappers
template <typename T> constexpr std::size_t size (T obj) { return obj.size(); }
template <typename T> constexpr auto sz1 (T obj) { return size(require_constexpr< obj.size() > { }); }
template <typename T> constexpr auto sz2 (T obj) { return size(require_constexpr< size(obj) > { }); }

int main0 (int, char**)
{
  constexpr auto const ar = std::array<int, 4u> { 4, 5, 6, 7 };

  // Check constexpr-computability of size(), sz1() and the expansion of sz2()
  noop<
    require_constexpr<
      size(require_constexpr< ar.size() > { }) + sz1(ar) +
      size(require_constexpr< size(ar)  > { })
    >
  >();

  // But this is an error
  // ERROR: "obj" is not a constexpr in sz2()
//noop< require_constexpr< sz2(ar) > >();

  return 0;
}

Edit Here is the relative compilation output.

clang

 src/main1.cpp:12:87: error: non-type template argument is not a constant expression
     template <typename T> constexpr auto sz2 (T obj) { return size(require_constexpr< size(obj) > { }); }
                                                                                       ^~~~~~~~~
 src/main1.cpp:28:32: note: in instantiation of function template specialization 'sz2<std::array<int, 4> >' requested here
       noop< require_constexpr< sz2(ar) > >();
                                ^
 src/main1.cpp:12:92: note: read of non-constexpr variable 'obj' is not allowed in a constant expression
     template <typename T> constexpr auto sz2 (T obj) { return size(require_constexpr< size(obj) > { }); }
                                                                                            ^
 src/main1.cpp:12:92: note: in call to 'array(obj)'
 src/main1.cpp:12:49: note: declared here
     template <typename T> constexpr auto sz2 (T obj) { return size(require_constexpr< size(obj) > { }); }
                                            ^

gcc

src/main1.cpp: In substitution of ‘template<long unsigned int N> using require_constexpr = std::make_index_sequence<N> [with long unsigned int N = size<std::array<int, 4ul> >(obj)]’:
src/main1.cpp:12:102:   required from ‘constexpr auto sz2(T) [with T = std::array<int, 4ul>]’
src/main1.cpp:28:38:   required from here
src/main1.cpp:12:102: error: ‘obj’ is not a constant expression
     template <typename T> constexpr auto sz2 (T obj) { return size(require_constexpr< size(obj) > { }); }
                                                                                                      ^
src/main1.cpp:12:102: note: in template argument for type ‘long unsigned int’ 
like image 258
Prikso NAI Avatar asked Aug 23 '14 18:08

Prikso NAI


People also ask

Can a function parameter be constexpr?

Like const , it can be applied to variables: A compiler error is raised when any code attempts to modify the value. Unlike const , constexpr can also be applied to functions and class constructors. constexpr indicates that the value, or return value, is constant and, where possible, is computed at compile time.

Can a constexpr function call a non constexpr function?

A call to a constexpr function produces the same result as a call to an equivalent non- constexpr function , except that a call to a constexpr function can appear in a constant expression. The main function cannot be declared with the constexpr specifier.

Do constexpr functions have to be defined in header?

Constexpr functions are implicitly inline, which means they are suitable to be defined in header files. Like any function in a header, the compiler is more likely to inline it than other functions.

Is constexpr function always inline?

2) A function defined entirely inside a class/struct/union definition, whether it's a member function or a non-member friend function, is always inline. 3) A function declared constexpr is always inline.


2 Answers

This looks like a bug with how the two compilers treat compiler-generated copy constructors.

This code compiles using both clang and g++:

#include <utility>

// utils
template <std::size_t N> struct require_constexpr { constexpr std::size_t size() const { return N; } };
struct test { 
  constexpr std::size_t size() const { return 0; } 
  constexpr test() { }
  constexpr test(const test &) { }
};
template <typename...> constexpr void noop (void) { }

// size() wrappers
template <typename T> constexpr std::size_t size (T obj) { return obj.size(); }
template <typename T> constexpr auto sz1 (T obj) { return size(require_constexpr< obj.size() > { }); }
template <typename T> constexpr auto sz2 (T obj) { return size(require_constexpr< size(obj) > { }); }

int main (int, char**)
{
  constexpr auto const ar = test();

  // Check constexpr-computability of size(), sz1() and the expansion of sz2()
  noop<
    require_constexpr<
      size(require_constexpr< ar.size() > { }) + sz1(ar) +
      size(require_constexpr< size(ar)  > { })
    >
  >();

  noop< require_constexpr< sz2(ar) > >();

  return 0;
}

But if we change the line

constexpr test(const test &) { }

to

constexpr test(const test &) = default;

Then it compiles in neither (g++, clang), even though there is absolutely no difference between what the two constructors do (test being a completely empty class), and §12.8 [class.copy]/p13 states that

If the implicitly-defined constructor would satisfy the requirements of a constexpr constructor (7.1.5), the implicitly-defined constructor is constexpr.

Furthermore, if the implicit copy constructor weren't constexpr, then the explicit-default declaration with constexpr should have caused the program to be ill-formed, with a diagnostic required (§8.4.2 [dcl.fct.def.default]/p2):

An explicitly-defaulted function may be declared constexpr only if it would have been implicitly declared as constexpr.

But both compilers (clang, g++) compile the second version of code if the second noop call is commented out.

like image 113
T.C. Avatar answered Sep 23 '22 15:09

T.C.


The key difference between sz1 and sz2 is that sz1 passes the address of obj to the size member function, which is not a valid result of a constant-expression but is fine as an intermediate result operand. sz2 performs an lvalue->rvalue conversion on obj for passing to the size function, and since obj is not constant this makes the expression non-constant.

T.C.'s point about implicit vs. explicit constructors is interesting. The source of the difference is that the implicit trivial copy constructor does a bitwise copy, which involves copying a (non-constant) byte of padding, whereas the user-provided copy constructor doesn't copy anything. But the standard says that the implicit constructor does a memberwise copy, so they ought to be treated the same.

What's not clear is whether they should both be rejected or both accepted; a strict reading of 5.19 suggests that both should be rejected, as both involve an lvalue->rvalue conversion for obj using the copy constructor. I've raised this issue with the C++ committee.

like image 23
Jason Merrill Avatar answered Sep 21 '22 15:09

Jason Merrill