Detecting a constexpr size() member function in C++ 14
Asked Answered
M

2

7

I have some generic code that would like to know when it has been passed a sequence of objects the number of which is known at compile time, as then it can choose an alternative algorithmic strategy. To that end I've tried writing a has_constexpr_size(T) constexpr function as below which attempts to probe T's size() member function to see if it can be executed as constexpr.

Note there is a key difference here from the usual "Can I detect a constexpr execution context?" questions because some STL containers like array<T> always provide a constexpr-available size() function whereas other STL containers like initializer_list<T> gain a constexpr-available size() function if and only if the initializer list is itself constexpr (because the implementation depends on the internal pointer pair, and those need to be constexpr for the size() to have all-constexpr inputs). The has_constexpr_size(T) function therefore takes an instance of the type T being tested in order that it can detect both situations.

Here is a testcase:

#include <array>
#include <type_traits>
#include <utility>
#include <vector>

namespace type_traits
{
  namespace detail
  {
    template <size_t N> struct Char
    {
      char foo[N];
    };
    template <class T> constexpr inline Char<2> constexpr_size(T &&v) { return (v.size(), true) ? Char<2>() : throw 0; }
    template <class T> inline Char<1> constexpr_size(...) { return Char<1>(); }
  }
  //! Returns true if the instance of v has a constexpr size()
  template <class T> constexpr inline bool has_constexpr_size(T &&v)
  {
    return noexcept(detail::constexpr_size<T>(std::forward<T>(v)));
  }

  // Non-constexpr array (always has a constexpr size())
  auto ca=std::array<int, 2>();
  // Constexpr initializer_list (has constexpr size()). Note fails to compile on VS2015 as its initializer_list isn't constexpr capable yet
  constexpr std::initializer_list<int> cil{1, 2};
  // Non-constexpr initializer_list (does not have constexpr size())
  std::initializer_list<int> il{1, 2};
  // Non-constexpr vector (never has constexpr size())
  std::vector<int> vec{1, 2};

  // Passes on GCC 4.9 and clang 3.8
  static_assert(ca.size(), "non-constexpr array size constexpr");
  // Passes on GCC 4.9 and clang 3.8
  static_assert(cil.size(), "constexpr il size constexpr");
  // Fails as you'd expect everywhere with non-constexpr il error
  //static_assert(il.size(), "non-constexpr il size constexpr");

  // Passes on GCC 4.9, fails on VS2015 and clang 3.8
  static_assert(has_constexpr_size(ca), "ca");  // Should NOT fail on VS2015 and clang 3.8
  // Fails on GCC 4.9 and clang 3.8. VS2015 doesn't apply.
  static_assert(has_constexpr_size(cil), "cil");  // FAILS, and it should not!
  // Passes, correct
  static_assert(!has_constexpr_size(il), "il");
  // Passes, correct
  static_assert(!has_constexpr_size(vec), "vec");

  constexpr bool test()
  {
    return has_constexpr_size(std::initializer_list<int>{1, 2});
  }
  constexpr bool testval=test();
  // Fails on GCC 4.9 and clang 3.8. VS2015 doesn't apply.
  static_assert(testval, "test()");
}

You'll note the direct static assertion of array<T>::size() and constexpr initializer_list<T>::size() works fine on all compilers, as you'd expect. My has_constexpr_size(T) function works fine for array<T>, but only on GCC 4.9 which I assume is because of GCC's particularly tolerant constexpr implementation. My has_constexpr_size(T) function fails for constexpr initializer_list<T> on all compilers.

So my questions are:

  1. Can a has_constexpr_size(T) function be written which correctly detects a constexpr-available size() member function for array<T> and constexpr initializer_list<T> and any other type providing a constexpr-available size() function?

  2. If (1) is not possible currently in C++ or in current compilers, can a has_constexpr_size(T) function be written which correctly detects a constexpr-available size() member function for array<T> and any other type providing an always constexpr-available size() function? Note this solution is not checking for whether simply some size() function exists, rather if it is constexpr-available. So has_constexpr_size(std::vector<int>()) would be false.

Mazonson answered 5/6, 2016 at 11:36 Comment(0)
M
0

Edit: The below answer is unfortunately wrong, if you substitute std::string as the type instead of int e.g. std::array<std::string, 2> you'll find it fails. For some reason which makes no sense to me, C++ 14 treats the following as this:

  static_assert(static_cast<std::array<int, 2> *>(0)->size(), "foo");          // passes
  static_assert(static_cast<std::array<std::string, 2> *>(0)->size(), "foo");  // fails

I am officially dumbfounded. Anyway, I'll leave the answer below for historical interest.


I have a working solution for my own question 2 where using Expression SFINAE I can detect a non-zero constexpr-available size() member function for array<> and correctly exclude that for vector<>, but it gets constexpr initializer_list<> wrong. Confirmed working on GCC 4.9 and clang 3.8 and VS2015:

#include <array>
#include <type_traits>
#include <utility>
#include <vector>

namespace type_traits {
  namespace detail {
    template <size_t N> struct Char { char foo[N]; };
    // Overload only available if a default constructed T has a constexpr-available non-zero size()
    template <class T, size_t N = T{}.size() + 1>
    constexpr inline Char<N> constexpr_size(const T &) {
      return Char<N>();
    }
    template <class T> constexpr inline Char<1> constexpr_size(...) {
      return Char<1>();
    }
  }
  //! Returns true if the instance of v has a constexpr size()
  template <class T> constexpr inline bool has_constexpr_size(const T &v) {
    return sizeof(detail::constexpr_size<typename std::decay<T>::type>(
               std::move(v))) > 1;
  }

  // Non-constexpr array (always has a constexpr size())
  auto ca = std::array<int, 2>();
  // Constexpr initializer_list (has constexpr size()). Note fails to compile on
  // VS2015 as its initializer_list isn't constexpr constructible yet
  #ifndef _MSC_VER
  constexpr std::initializer_list<int> cil{1, 2};
  #endif
  // Non-constexpr initializer_list (does not have constexpr size())
  std::initializer_list<int> il{1, 2};
  // Non-constexpr vector (never has constexpr size())
  std::vector<int> vec{1, 2};

  // Correct on GCC 4.9 and clang 3.8 and VS2015
  static_assert(ca.size(), "non-constexpr array size constexpr");
  // Correct on GCC 4.9 and clang 3.8.
  #ifndef _MSC_VER
  static_assert(cil.size(), "constexpr il size constexpr");
  #endif
  // Fails as you'd expect everywhere with non-constexpr il error
  // static_assert(il.size(), "non-constexpr il size constexpr");

  // Correct on GCC 4.9 and clang 3.8 and VS2015
  static_assert(has_constexpr_size(ca), "ca");
  // Incorrect on GCC 4.9 and clang 3.8 and VS2015
  #ifndef _MSC_VER
  static_assert(!has_constexpr_size(cil), "cil");  // INCORRECT!
  #endif
  // Correct on GCC 4.9 and clang 3.8 and VS2015
  static_assert(!has_constexpr_size(il), "il");
  // Correct on GCC 4.9 and clang 3.8 and VS2015
  static_assert(!has_constexpr_size(vec), "vec");

  constexpr bool test_ca() {
    return has_constexpr_size(std::array<int, 2>{1, 2});
  }
  constexpr bool testca = test_ca();
  // Correct on GCC 4.9 and clang 3.8 and VS2015
  static_assert(testca, "testca()");

  constexpr bool test_cil() {
    return has_constexpr_size(std::initializer_list<int>{1, 2});
  }
  constexpr bool testcil = test_cil();
  // Incorrect on GCC 4.9 and clang 3.8 and VS2015
  static_assert(!testcil, "testcil()");          // INCORRECT!
}

So all we need now is some method of filtering out non-constexpr input types so we can correctly return false for non-constexpr input. I suspect this is the same unsolved problem as elsewhere on stackoverflow :(

Any further advancement, or thoughts, is very welcome.

Mazonson answered 5/6, 2016 at 17:23 Comment(3)
Are there cases where remove_cv after decay is not redundant?Aurie
I thought decay doesn't always remove cv qualifiers. Checking ... en.cppreference.com/w/cpp/types/decay says cv is only removed if the type is not an array and not a function pointer. I agree above it's probably redundant, but I was making very sure the routine only ever sees non-const non-volatile types to avoid surprises.Mazonson
@MarcGlisse I've improved the answer, removing the remove_cv.Mazonson
F
0

If I'm interpreting this proposal:

std::is_constant_evaluated()

correctly, the standard's wording as of C++14 does not even clarify what must be known at compile time and what doesn't. And, at any rate, this "magical library function", added in C++20, could not be implemented in the language itself.

The GCC 9 implementation is:

constexpr inline bool
is_constant_evaluated() noexcept
{ return __builtin_is_constant_evaluated(); }

which is another indication (although not a certain one) that this can't be implemented using other language constructs.

Frank answered 9/6, 2020 at 7:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.