Search code examples
c++constexprc++20if-constexpr

Using a `constexpr` function on a variable passed as lvalue in a non-constexpr function


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?


Solution

  • 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.