c++c++14constexpr

Why isn't the compiler evaluating a constexpr function during compilation when every information is given?


This

int main()
{
  std::cout << range(1, 11).reverse().sort().sum() << std::endl;
}

is everything in main, and as the code says, it creates a list from 1 to 10 inclusive, reverses it, un-reverses it by sorting, and calculates the sum. The output should thus be 55.

The code is rather an experiment, (ab)using the relaxed requirements of constexpr in C++14. I did my best to create a compile-time list class, but really couldn't go far enough. The class is incomplete, but it still can imitate a lot of functional style programming.

As far as I understand, the requirements of constexpr are there to let the compiler evaluate things in compile-time. So I thought the compiler can simply replace everything with a constant 55 for my code, but it didn't. The code really has everything it needs to get the result in compile-time. What am I missing?

From the comments, I tried to check the problem by using the result in static_assert. clang and gcc both gives me an error, but I failed to understand either, and the latter seems to be broken...

clang

a.cpp:142:17: error: static_assert expression is not an integral constant expression
  static_assert(range(1, 11).reverse().sort().sum() == 55, "");
                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
a.cpp:60:23: note: assignment to object outside its lifetime is not allowed in a constant
      expression
  l.array[l.length++] = t;
                      ^
a.cpp:134:9: note: in call to '&l->add(1)'
    l = l.add(a);
        ^
a.cpp:142:17: note: in call to 'range(1, 11, 1)'
  static_assert(range(1, 11).reverse().sort().sum() == 55, "");
                ^
1 error generated.

gcc

main.cpp: In function 'int main()':

main.cpp:142:3: error: non-constant condition for static assertion

   static_assert(range(1, 11).reverse().sort().sum() == 55, "");

   ^

main.cpp:142:38: error: 'constexpr List<T> List<T>::reverse() const [with T = int]' called in a constant expression

   static_assert(range(1, 11).reverse().sort().sum() == 55, "");

                                      ^

main.cpp:74:19: note: 'constexpr List<T> List<T>::reverse() const [with T = int]' is not usable as a constexpr function because:

 constexpr List<T> List<T>::reverse() const

                   ^

main.cpp:74:19: sorry, unimplemented: unexpected AST of kind result_decl

main.cpp:74: confused by earlier errors, bailing out

full code

#include <cstdint>
#include <iostream>
#include <limits>
#include <algorithm>
#include <initializer_list>

template<typename T>
class List
{
  template<typename T2>
  friend std::ostream &operator<<(std::ostream &, const List<T2> &);
public:
  constexpr List();
  constexpr List(std::initializer_list<T>);
  constexpr T head() const;
  constexpr List<T> tail() const;
  constexpr List<T> add(T) const;
  constexpr List<T> merge(List<T>) const;
  constexpr List<T> reverse() const;
  template<typename Filter>
  constexpr List<T> filter(Filter) const;
  constexpr List<T> sort() const;
  constexpr T sum() const;
private:
  int length;
  T array[0x100];
};

template<typename T>
constexpr List<T>::List()
: length(0)
{
}

template<typename T>
constexpr List<T>::List(std::initializer_list<T> l)
: length {static_cast<int>(l.size())}
{
  std::copy(l.begin(), l.end(), array);
}

template<typename T>
constexpr T List<T>::head() const
{
  return array[0];
}

template<typename T>
constexpr List<T> List<T>::tail() const
{
  List<T> l;
  l.length = length - 1;
  std::copy_n(array + 1, l.length, l.array);
  return l;
}

template<typename T>
constexpr List<T> List<T>::add(T t) const
{
  List<T> l {*this};
  l.array[l.length++] = t;
  return l;
}

template<typename T>
constexpr List<T> List<T>::merge(List<T> l) const
{
  std::copy_backward(l.array, l.array + l.length, l.array + l.length + length);
  std::copy_n(array, length, l.array);
  l.length += length;
  return l;
}

template<typename T>
constexpr List<T> List<T>::reverse() const
{
  List<T> l;
  l.length = length;
  std::reverse_copy(array, array + length, l.array);
  return l;
}

template<typename T>
template<typename Filter>
constexpr List<T> List<T>::filter(Filter f) const
{
  List<T> l;
  for (int i {0}; i < length; ++i)
  {
    if (f(array[i]))
    {
      l = l.add(array[i]);
    }
  }
  return l;
}

template<typename T>
constexpr List<T> List<T>::sort() const
{
  if (length == 0)
  {
    return *this;
  }
  return tail().filter([&](T t) {return t < head();}).sort().add(head())
  .merge(tail().filter([&](T t) {return t >= head();}).sort());
}

template<typename T>
constexpr T List<T>::sum() const
{
  if (length == 0)
  {
    return T {};
  }
  return head() + tail().sum();
}

template<typename T>
std::ostream &operator<<(std::ostream &os, const List<T> &l)
{
  os << '{';
  for (int i {0}; i < l.length - 1; ++i)
  {
    os << l.array[i] << ", ";
  }
  return os << l.array[l.length - 1] << '}';
}

inline constexpr List<int> range(int a, int b, int c = 1)
{
  List<int> l;
  while (a < b)
  {
    l = l.add(a);
    a += c;
  }
  return l;
}

int main()
{
  static_assert(range(1, 11).reverse().sort().sum(), "");
  std::cout << range(1, 11).reverse().sort().sum() << std::endl;
}

Solution

  • The initial error is that array is not initialized in your constructor. You can fix this by initializing it:

    template<typename T>
    constexpr List<T>::List()
    : length(0)
    , array{}
    {
    }
    

    After that, you will run into the problem that the <algorithm> functions you use are not constexpr; you can fix this by copying the example definitions from the standard into your implementation and marking your copies constexpr:

    template<class BidirIt, class OutputIt>
    constexpr OutputIt reverse_copy(BidirIt first, BidirIt last, OutputIt d_first)
    {
        while (first != last) {
            *(d_first++) = *(--last);
        }
        return d_first;
    }
    
    // etcetera
    

    Finally, you're using lambdas as filter predicates (in sort); lambdas are illegal in a constant-expression. The fix here is to expand the lambdas by hand into function objects:

    template<typename T>
    constexpr List<T> List<T>::sort() const
    {
      if (length == 0)
      {
        return *this;
      }
      T pivot = head();
      struct Lt { T pivot; constexpr bool operator()(T t) const { return t < pivot; } };
      struct Ge { T pivot; constexpr bool operator()(T t) const { return t >= pivot; } };
      return tail().filter(Lt{pivot}).sort().add(pivot)
        .merge(tail().filter(Ge{pivot}).sort());
    }
    

    With these changes your code will compile under clang 3.7, though not gcc 5.2.0.


    gcc 5.2.0 has two bugs:

    First, it doesn't like the combined decrement-indirection *(--last) in reverse_copy; this is easily fixed:

    template<class BidirIt, class OutputIt>
    constexpr OutputIt reverse_copy(BidirIt first, BidirIt last, OutputIt d_first)
    {
        while (first != last) {
            --last;
            *(d_first++) = *last;
        }
        return d_first;
    }
    

    Second, it doesn't like comparing pointers into array; however, it can be satisfied by changing array + length to &array[length].

    Live example with these changes (works with clang 3.7 and gcc 5.2.0).