12
votes

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’ 
2
interesting. I would be surprised if that was the intended standard behaviour. I can guess it’s a bug in both compiler…? Hard to believe since it happens the same in both compilers, not so hard to believe considering constexpr is relatively newly implemented. - bolov

2 Answers

5
votes

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.

5
votes

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.