Saswat Mishra
Saswat Mishra

Reputation: 195

constexpr operator overloading issues with using arguments

I am making a simple class inheriting from std::array. The point is that it should throw a compile time error if the subscript operator is used for an out of bounds index. However, I keep getting an error message. This is the code simplified.

#include <array>

using namespace std;

template<typename type, size_t size>
struct container : array<type,size>
{
    constexpr inline type& operator[](int index) const
    {
        static_assert(index<size,"");
        return ((static_cast<const array<type,size> >(*this))[index]);
    }

    template<class... bracelist>
    constexpr container(bracelist&&... B)
    :array<type,size>{std::forward<bracelist>(B)...}
    {}

    container() = default;
};

int main()
{
    constexpr container<int,4> myarray = {5,6,7,8};
    constexpr int number = myarray[2];
}

The error it gives me is:

main.cpp|80|error: non-constant condition for static assertion
main.cpp|80|error: 'index' is not a constant expression

However, I used "index" in the return statement, and commenting out the static_assert makes it work fine. If index was not a constant expression, wouldn't I not be able to use it in the subscript operator for std::array after the static_cast? I am new to using the constexpr functionality, so any help would be appreciated. Thank you.

Note: I am aware std::array's constexpr subscript operator already does this, I just want to know how to do this for future uses. Thanks.

Upvotes: 2

Views: 1764

Answers (3)

Richard Hodges
Richard Hodges

Reputation: 69864

There are 2 really useful features of constexpr functions, the interplay of which is not always fully appreciated.

  • In constexpr context they only evaluate code paths that are taken for the constexpr arguments.

  • In non-constexpr context they behave exactly like regular functions.

Which means that we can use exceptions to great effect.

Since while in constexpr context, if the exception path is taken, this is a compiler error (throw is not allowed in constexpr context). You get to see the "exception" in your compiler's error output.

example:

#include <array>
#include <stdexcept>

template<typename type, std::size_t size>
struct container : std::array<type,size>
{
    constexpr auto operator[](std::size_t index) const
    -> type const&
    {
        if (index < size)
            return static_cast<const std::array<type,size>>(*this)[index];
        else
            throw std::out_of_range("index out of range" + std::to_string(index));
    }

    template<class... bracelist>
    constexpr container(bracelist&&... B)
    : std::array<type,size>{std::forward<bracelist>(B)...}
    {}

    container() = default;

};

int main()
{
    constexpr container<int,4> myarray = {5,6,7,8};
    constexpr int number = myarray[4];
}

Example output:

main.cpp: In function 'int main()':
main.cpp:28:37:   in 'constexpr' expansion of 'myarray.container<int, 4>::operator[](4)'
main.cpp:13:81: error: expression '<throw-expression>' is not a constant expression
             throw std::out_of_range("index out of range" + std::to_string(index));

This approach is actually more versatile than static_assert, since it works at both compile and runtime.

Upvotes: 2

Lightness Races in Orbit
Lightness Races in Orbit

Reputation: 385104

This is the reason for std::get<N>(array) — it is the only way to assuredly pass a "compile-time value" in a manner that'll satisfy the rules of the language. Your attempt to create a compile-time op[] cannot work. You could of course make your own templated accessor like std::get, but one might ask why not just use std::array as it is already.

Upvotes: 0

bolov
bolov

Reputation: 75688

The thing you have to keep in mind is that constexpr functions can be called at runtime with non constexpr arguments. constexpr means for a function that the function is usable in a compile-time evaluated expression (e.g. another constexpr or a template argument) but not exclusively. A constexpr function can still be called in the classical way, i.e. at run-time with run-time variables. Which means that the parameters of a constexpr function cannot be and are not compile-time constants.

It doesn't apply to your case but in general if you know a parameter will always be called with a compile time constant than you can make it a template parameter.

constexpr void foo(int a)
{
    static_assert(a != 0); // illegal code because the parameter 
                           // cannot be a compile time constant
}

void test()
{
    int x;
    std::cin >> x;
    foo(x); // this is perfectly legal
}
template <int N>
void foo()
{
    static_assert(N != 0); // legal
}

void test()
{
    int x;
    std::cin >> x;
    foo<x>(); // illegal, x is not a compile time constant


    foo<24>(); // legal
    constexpr int c = 11;
    foo<c>();; // legal
}

Upvotes: 1

Related Questions