分配给数组子部分:我在这里分配给右值吗?如果是,我该如何解决?

Assignment to an array subsection: am I assigning to an Rvalue here, and if so how do I fix it?

希望有朝一日让我的 Fortran 代码更容易移植到 C++,我一直在研究一些表达式模板代码以提供整个数组算术运算符以及从指定部分复制和分配给指定部分的能力更长的阵列。不幸的是,如果没有相当多的样板代码,我想不出一种方法来回答我的问题,我会尽可能地减少这些样板代码。

首先我有一个非常简单的 'C-style array struct' 封装了一个指针和一个长度,适合在我的混合程序的 C、Fortran、C++ 和 Java 部分之间轻松地来回传递-语言应用:

typedef struct {
    int *p;    /*!< Pointer to the data */
    int n;     /*!< The number of elements; int, not size_t, for Fortran compatibility  */
} int_array_C;

typedef struct {
    float *p;    /*!< Pointer to the data */
    int n;       /*!< The number of elements; int, not size_t, for Fortran compatibility  */
} float_array_C;

typedef struct {
    double *p;   /*!< Pointer to the data */
    int n;       /*!< The number of elements; int, not size_t, for Fortran compatibility  */
} double_array_C;

...所有原生类型依此类推。然后,我根据 Wikipedia entry on that subject:

中建议的方法定义了一些非常简单的表达式模板
template <typename E, typename T_C >
class VecExpression
{
    typedef typename std::remove_pointer<decltype(T_C::p)>::type TT;
public:
    //! Returns a const reference to the i'th element in the array
    TT operator[] (int i) const noexcept 
    {
        return static_cast<E const&>(*this)[i];
    }

    //! Returns the total size of the array
    int size() const noexcept
    {
        return static_cast<E const &>(*this).size();
    }

    operator E&() { return static_cast<E&>(*this); }
    operator E const&() const { return static_cast<const E&>(*this); }
};

template <typename E1, typename T_C, typename E2, typename U_C  >
class VecSum : public VecExpression< VecSum<E1, T_C, E2, U_C>, T_C >
{
    E1 const & _u;
    E2 const & _v;
public:
    //! Constructor taking two VecExpressions
    VecSum(VecExpression<E1, T_C> const& u, VecExpression<E2, U_C> const &v) : _u(u), _v(v)
    {
        assert(u.size() == v.size());
    }

    int size() const noexcept { return _v.size(); }

    auto operator[](int i) const
        -> const decltype(_u[i] + _v[i]) { return _u[i] + _v[i]; }
                 // Automatically takes care of type promotion e.g. int to double
                 // according to the compiler's normal rules
};

template <typename E1, typename T_C, typename E2, typename U_C  >
VecSum<E1, T_C, E2, U_C> const operator+(VecExpression<E1, T_C> const &u,
                                         VecExpression<E2, U_C> const &v)
{
    return VecSum<E1, T_C, E2, U_C>(u, v);
}

为了给我一种操作 C 风格向量内容的方法,我定义了一些模板:一个模板在预先存在的缓冲区中操作数据,另一个模板使用 std::vector:

template <typename T_C> class nArray : public T_C, public VecExpression<nArray <T_C>, T_C >
{                                                  // This is the 'curiously recurring template
                                                   // pattern' (CRTP)
    typedef typename std::remove_pointer<decltype(T_C::p)>::type TT;

    struct startingIndex : public T_C
    {
        size_t start;

        startingIndex(const T_C *initialiser) noexcept
        {
            *(static_cast<T_C *>(this)) = *initialiser;
        }

        nArray to(int element) noexcept
        {
            T_C::n = element - start + 1;
            nArray<T_C> newArray(*(static_cast<T_C *>(this)));
            return newArray;
        }
    };

public:
    //! Constructor to create an nArray from an array_C, without copying its memory
    nArray(T_C theArray) noexcept
    {
        T_C::p = theArray.p;
        T_C::n = theArray.n;
    }

    //! Constructor to create an nArray from an ordinary C array, without copying its memory
    template<std::size_t N>
    nArray(TT (&theArray)[N]) noexcept
    {
        T_C::p = &theArray[0];
        T_C::n = N;
    }

    nArray & operator=(VecExpression<nArray<T_C>, T_C> const& source) &
    {
        // Note that we cannot use the copy-and-swap idiom here because we don't have the means to
        // construct a new temporary memory buffer. Therefore we have to handle the assignment-to-self
        // case explicitly.
        if (&source == this) return *this;
        assert(T_C::n == source.size());
        for (int i=0; i<T_C::n; ++i) T_C::p[i] = source[i];
        return *this;
    }

    //! Copy assignment operator taking a VecExpression of a different (but compatible) type
    //! without allocating any new memory
    template <typename E, typename U_C>
    nArray operator=(VecExpression<E, U_C> const& source) &
    {
        assert(T_C::n == source.size());
        for (int i=0; i<T_C::n; ++i) T_C::p[i] = static_cast<TT>(source[i]);
        return *this;
    }

    //! Returns a non-const reference to the i'th element in the array
    TT& operator[] (int i) noexcept
    {
        return T_C::p[i];
    }

    //! Returns a const reference to the i'th element in the array
    const TT& operator[] (int i) const noexcept
    {
        return T_C::p[i];
    }

    startingIndex from(int element) const noexcept
    {
        startingIndex theNewArray(this);
        theNewArray.p = &T_C::p[static_cast<size_t>(element)];
        theNewArray.n = T_C::n - element;
        theNewArray.start = element;
        return theNewArray;
    }

    nArray to(int element) const noexcept
    {
        nArray theNewArray;
        theNewArray.p = T_C::p;
        theNewArray.n = element + 1;
        return theNewArray;
    }

    // ... and a whole bunch of other functions
};

template <typename T_C> class nVector : public nArray<T_C>
{
    typedef typename std::remove_pointer<decltype(T_C::p)>::type TT;

public:
    template<std::size_t N>
    nVector(TT (&source)[N]) 
    {
        contents.resize(N);
        update_basetype();
        std::copy(&source[0], &source[N], contents.begin());
    }

    // ...and a whole bunch of other constructors and assignment operators
    // which echo those of nArray with the additional step of resizing the
    // internal std::vector and copying the contents into it

private:
    void update_basetype() noexcept
    {
        T_C::p = contents.size() > 0 ? contents.data() : nullptr;
        T_C::n = contents.size();
    }

    std::vector<TT> contents;
};

typedef nArray<float_array_C> float_array;
typedef nVector<float_array_C> float_vector;

// ...and so on

呸!从这一点开始,我可以做类似

的事情
float a[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f };
float b[] = { 9.0f, 8.0f, 7.0f, 6.0f, 5.0f, 4.0f };

float_array aArray(a);  // The array contents aren't copied, only
float_array bArray(b);  // the pointers

float_vector aVector = aArray.from(2);  // aVector is { 3.0f, 4.0f, 5.0f, 6.0f }
float_vector bVector = bArray.to(3);    // bVector is { 9.0f, 8.0f, 7.0f, 6.0f } 
float_vector cVector = aArray.from(2).to(4) + bArray.from(1).to(3);
                                        // cVector is { 11.0f, 11.0f, 11.0f } 

...他们工作很愉快。现在,我终于可以回答我的问题了。假设我要赋值给一个数组子段,例如:

float_vector dVector(10);  // An empty 10-element array
dVector.from(3).to(5) = aArray.from(2).to(4) + bArray.from(1).to(3);

事实上,如果我在 Visual C++ 2013 中编译,这工作得很好,但在 gcc 中却不行。编译在分配时失败,并显示消息:

error: no match for 'operator=' (operand types are 'nArray<float_array_C>' and 'const VecSum<nArray<float_array_C>, float_array_C, nArray<float_array_C>, float_array_C>')
note: candidates are:
     < ...skipping over a long list of utterly implausible options>
note: nArray<T_C>& nArray<T_C>::operator=(const VecExpression<nArray<T_C>, T_C>&) & [with T_C = float_array_C]
note: no known conversion for implicit 'this' parameter form 'nArray<float_array_C>' to 'nArray<float_array_C>&'

现在,当尝试将临时对象分配给非常量引用或尝试对右值进行分配时,此错误消息似乎会出现在文献中,并且 Visual C++ 被记录为懒惰就此规则而言,gcc 是(自然地,gcc 是符合标准的规则)。我可以理解为什么编译器会考虑

dVector.from(3).to(5)

作为右值,尽管我竭尽全力试图阻止它成为这样。例如,我的 startingIndex::to() 方法努力 return 通过值而不是引用来创建 nArray 对象,如果我写

auto test1 = dVector.from(3).to(5);
auto test2 = aArray.from(2).to(4) + bArray.from(1).to(3);
test1 = test2;

...然后这工作正常,编译器告诉我 'test1' 是一个 'nArray<float_array_C>'(即 float_array),它应该是这样。

所以,我的问题是:我真的犯了试图在这里分配右值的错误吗?如果我是,我怎么能停止这样做,同时仍然能够以这种方式或至少以某种类似可读的方式对子数组进行分配。我真的希望这可以在 C++ 中以某种方式完成,否则我想我需要回到 Fortran 领域,写

dVector(3:5) = aArray(2:4) + bArray(1:3)

...从此过上幸福的生活。

更新

根据 Chris Dodd 的建议,我为进一步的 nArray 构造函数尝试了几种不同的形式,并确定了:

nArray && operator=(VecExpression<nArray<T_C>, T_C> const& source) &&
{
    if (&source == this) return *this;
    assert(T_C::n == source.size());
    for (int i=0; i<T_C::n; ++i) T_C::p[i] = source[i];
    return *this;
}

(暂且不支持赋值自检)。 gcc 编译器似乎通过了这个,但我得到的下一个错误是:

no known conversion for argument 1 from 'const VecSum<nArray<float_array_C>, float_array_C, nArray<float_array_C>, float_array_C>' to 'const VecExpression<nArray<float_array_C>, float_array_C>&'

这至少是一条不同的消息(始终是进步的标志),但它仍然暗示基本问题是对错误类型的引用的分配。不幸的是,我不确定我应该在哪里寻找这个:我尝试让我的 operator+ return a VecSum const & 而不是 by -value return,但这完全没有区别。现在我又被卡住了,所以我的下一个策略是在我的 Linux 分区中安装 clang,看看我是否从中得到更有用的错误消息...

进一步更新:

Clang 不是特别有用;它只能说:

candidate function not viable: no known conversion from 'nArray<[...]>' to 'nArray<[...]>' for object argument

没有提供太多线索!

最终更新:

事实上,回想起来,解决方案竟然如此明显,我对此感到非常尴尬。我所需要做的就是为我的赋值运算符提供与普通移动赋值运算符完全相同的形式:

nArray & operator=(VecExpression<nArray<T_C>, T_C> const& source) &&
{
    // Better assignment-to-self check pending...
    assert(T_C::n == source.size());
    for (int i=0; i<T_C::n; ++i) T_C::p[i] = source[i];
    return *this;
}

这当然是 Chris Dodd 首先建议的逐字记录,并且在 Linux 和 Windows 上的 clang 和 gcc 中工作得非常好。

您的 nArray 赋值运算符:

nArray & operator=(VecExpression<nArray<T_C>, T_C> const& source) &

被明确定义为仅适用于左值 nArray 对象(该行的最后一个 &),而不适用于右值对象,因此不能用于分配给临时切片,例如正如你从 dVector.from(3).to(5) 得到的那样。您需要一个赋值运算符,其右值引用为 "this":

nArray & operator=(VecExpression<nArray<T_C>, T_C> const& source) &&

为了能够像这样在临时切片上调用此赋值运算符。

请注意,您的 &source == this 自我分配检查不充分。您可能有不同的 nArray 个对象,这些对象引用相同的底层存储,但切片不同。考虑一下如果您尝试类似

会发生什么
aVector.from(3).to(7) = aVector.from(1).to(5)