1

I am using std::array as the base for representing vectors that have fixed length at compile time, and want to use std::array::size as a constexpr function to disable the calculation of cross product for 1D and 2D vectors.

When I use std::array::size in a non-constexpr function, that takes on my vectors as lvalue arguments, I get an error:

main.cpp: In instantiation of ‘VectorType cross(const VectorType&, const VectorType&) [with VectorType = Vector<double, 3>]’:
main.cpp:97:16:   required from here
main.cpp:89:62: error: ‘vec1’ is not a constant expression
   89 |     return cross_dispatch<std::size(vec1), VectorType>::apply(vec1, vec2);
      |            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~
main.cpp:89:36: note: in template argument for type ‘long unsigned int’
   89 |     return cross_dispatch<std::size(vec1), VectorType>::apply(vec1, vec2);
      |            

Here is the minimal working example with the main function:

#include <array>
#include <iostream>

using namespace std;

template<typename AT, auto D> 
class Vector final
: 
    public std::array<AT, D> 
{

public: 

    using container_type = std::array<AT,D>; 
    using container_type::container_type; 

    template<typename ... Args>
    constexpr Vector(Args&& ... args)
        : 
            container_type{std::forward<Args>(args)...}
    {}

    // Delete new operator to prevent undefined behavior for
    // std::array*= new Vector; delete array; std::array has 
    // no virtual destructors.
    template<typename ...Args>
    void* operator new (size_t, Args...) = delete;

};

using vector = Vector<double, 3>; 

template<std::size_t DIM, typename VectorType> 
struct cross_dispatch
{
    static VectorType apply(VectorType const& v1, VectorType const& v2)
    {
        static_assert(std::size(v1) < 3, "Cross product not implemented for 2D and 1D vectors."); 
        static_assert(std::size(v1) > 3, "Cross product not implemented for ND vectors."); 
        return VectorType();
    }
};

template<typename VectorType> 
struct cross_dispatch<3, VectorType>
{
    static VectorType apply(VectorType const& v1, VectorType const& v2)
    {
        return VectorType(v1[1]*v2[2] - v1[2]*v2[1], 
                          v1[2]*v2[0] - v1[0]*v2[2], 
                          v1[0]*v2[1] - v1[1]*v2[0]);

    }
};

template <typename VectorType> 
VectorType cross(VectorType const& vec1, VectorType const& vec2) 
{
    return cross_dispatch<std::size(vec1), VectorType>::apply(vec1, vec2);  
}

int main()
{
    vector p1 {1.,2.,3.}; 
    vector q1 {1.,2.,3.}; 

    cross(p1,q1);
}

I found this question that mentions a bug in GCC 8.0, but I am using g++ (GCC) 10.1.0.

To quote the answer

An expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine (6.8.1), would evaluate one of the following expressions:

... an id-expression that refers to a variable or data member of reference type unless the reference has a preceding initialization and either it is initialized with a constant expression or its lifetime began within the evaluation of e

Does this mean, in human (non-standard) language, that in my expression e:=cross(p1,p2), p1 and p2 are not initialized before as constexpr and their lifetime did not begin with e, so even though p1 and p2 are objects of a data type whose size is known at compile time nad whose mfunction size is a constexpr mfunction, I now have to declare them as constexpr before binding them as lvalues in a function that's not constexpr?

tmaric
  • 5,347
  • 4
  • 42
  • 75
  • 3
    This isn't especially minimal, and the question title talks about `if constexpr` but there's no `if constexpr` in this example? – Barry Jun 23 '20 at 13:30
  • 1
    Hmm, tl;dr. You can however add `static constexpr size_t size() { return D; }` to your `Vector` and do `return cross_dispatch::apply(vec1, vec2);` to get this particular problem solved. – Ted Lyngmo Jun 23 '20 at 13:44
  • I edited the title, there was another function template with `if constexpr` but I ended up using `cross`. Is `Vector` not inheriting also the `static constexpr size_t size()` from `std::array`, or did I forget another rule? – tmaric Jun 23 '20 at 13:57
  • Is `std::array::size()` really static? I don't think it is - and even if it is, I don't think calling it via an instance (`std::size(vec1)`) works and that you have to call it like `VectorType::size()` – Ted Lyngmo Jun 23 '20 at 14:40
  • @TedLyngmo: yes, it's not static. my code calls `std::size` (https://en.cppreference.com/w/cpp/iterator/size) which calls `std::array::size` member function, I think. – tmaric Jun 23 '20 at 14:42
  • Yes, that's what `std::size` does in this case and even `static` `size()` functions are called via the instance too. – Ted Lyngmo Jun 23 '20 at 15:17
  • 2
    You will be interested by this blog post [The constexpr array size problem](https://brevzin.github.io/c++/2020/02/05/constexpr-array-size/), the author is the first commenter! @Barry – Oliv Jun 23 '20 at 16:00

1 Answers1

1

Below, I answer as to why your code doesn't work. Focusing on your use-case: as the others have said, std::array::size is not static, and all std::size does is call that non-static function. Your best bet is to simply add a static size function to your Vector class:

static constexpr auto size() {
    return D;
}

Your implementation of cross will not work because you cannot use non-constant expressions to initialize templates. See this SO answer on why function arguments are not constant expressions.

Basically, calling your cross function requires generating a new instance of the cross_dispatch structure for every different value of std::size(vec1), which also requires knowing the address of every given vec1 at compile-time since std::size calls a non-static function. You should be able to see from this that the compiler simply can't know which instances of cross_dispatch need to be created.

Above, I provided a solution specific to your use-case. If you were doing more than measuring the size of your Vector, a second solution would involve passing the objects as template parameters instead (which will require them to be static):

template <typename VectorType, VectorType const& vec1, VectorType const& vec2>
constexpr VectorType cross()
{
    return cross_dispatch<std::size(vec1), VectorType>::apply(vec1, vec2);  
}

int main()
{
    static vector p1 {1.,2.,3.}; 
    static vector q1 {1.,2.,3.}; 

    cross<vector, p1, q1>();
}

Because p1 and q1 are static, their addresses can be known at compile-time, allowing cross's template to be initialized. Template parameters do not change at run-time, so std::size(vec1) is now a constant expression.

clyne
  • 682
  • 4
  • 11