Rcpp: Behavior of operator[], operator(), and at() across class and arg number

Created on 8 Dec 2016  路  4Comments  路  Source: RcppCore/Rcpp

Looking at the discussion of bounds checking in "Element Access" unoffical API docs by @coatless:
"Note: Unlike R, there is no [] subset operator for matrices with C++"

At present, operator[] compiles with any number of args, both for Vector and Matrix.
I tried looking at the doxygen docs to figure out what was actually happening here.
Focusing on Vector, I see:

  • 3 pairs of functions differing by constness: R_xlen_t, std::string &, and VectorBase<...>&
  • Finally: Range &

Questions:

  1. Single-argument versions of operator() and at() don't compile for Matrix, so that operator[] is the only way to get linear offset for Matrix objects, with no default options for Matrix linear-offset-with-bounds-checking. Is this intentional?
  2. Which overload of operator[] is actually being called for nargs > length(dim) (example below)?
  3. Is it possible for operator[] to not compile (or to throw) when given too many args, without breaking existing code?
  4. If not 3., then might it make sense to strongly recommending against using operator[], particularly for matrices, possibly to the extent of deprecating it? The current behavior seems like trouble waiting to happen.

Complex relationship between operator[], operator(), and at():

As per the unofficial docs referenced above, operator() checks bounds for 1 arg (Vector), but not 2 args (Matrix). With an eye to best practices, is it practical for:

  1. operator() = no bounds checks, and at() = bounds checks, regardless of class or arg #,
  2. and for operator() and at() to take 1 args for Matrix, yielding the present behavior of Matrix::operator[], with & without bounds checks?

This would permit (unofficially) deprecating operator[], and rather simplify explanations.

Full example

library(Rcpp)
sourceCpp('checkops.cpp');
checkops(a_mat=matrix(1:9, ncol=3), a_vec=1:20)

checkops.cpp:

#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
void checkops(NumericMatrix a_mat, NumericVector a_vec){
    // no BC
    auto aa = a_mat(1,2);
    // not defined?
    // auto aa1 = a_mat(1);
    // BC
    auto bb = a_mat.at(1,2);
    // not defined?
    //auto bb = a_mat.at(1);
    // fails correctly
    // auto bb2 = a_mat.at(1,2,3);
    // compiles, badsauce
    auto cc = a_mat[1,2];
    // ??
    auto dd = a_mat[1,2,3];
    // vector 
    // BC
    auto ff = a_vec(1);
    // also BC, identical to above
    auto gg = a_vec.at(1);
    // fails correctly
    //auto hh = a_vec.at(1,2);
    // No BC
    auto ii = a_vec[1];
    // compiles, badsauce
    auto jj = a_vec[1,2];
    // output
    Rcout << "Matrix: " << aa << bb << cc << dd << std::endl ;
    //
    Rcout << "Vector: " << ff << gg << ii << jj << std::endl;
}

Most helpful comment

And not to nitpick, but at the moment the unofficial API docs you've cited are, well, unofficial, and there is still a fair bit more to be done (in terms of breadth of content as well as the actual verbiage used) before they are ready to be promoted as official documentation. Having said that, your feedback is much appreciated and will certainly be taken into account during further development of the documentation.

All 4 comments

Regarding

Is it possible for operator[] to not compile (or to throw) when given too many args, without breaking existing code?

The short answer is probably not, and not for any reason inherent to the design of these classes / their definition of operator[], but rather due to how operator, behaves in standard C++ itself. When you write a_mat[1,2,3], 1 and 2 are "silently" discarded, resulting in the expression a_mat[3], which is valid.
See the explanation here:

In a comma expression E1, E2, the expression E1 is evaluated, its result is discarded, and its side effects are completed before evaluation of the expression E2 begins [...]

For example,

#include <Rcpp.h>

// [[Rcpp::export]]
double bounds() {
    Rcpp::NumericMatrix m(2, 2);
    m.column(0) = Rcpp::NumericVector::create(1, 1);
    m.column(1) = Rcpp::NumericVector::create(2, 2);

    return m[0, 1, 2];      // evaluates as m::operator[](2)
}

/*** R

R> bounds()
# [1] 2

*/

If you are compiling with the -Wall flag (which you always should be), you will be warned about this:

rcppbounds2.cpp:9:17: warning: left operand of comma operator has no effect [-Wunused-value]
return m[0, 1, 2]; // evaluates as m::operator[2]
^
rcppbounds2.cpp:9:20: warning: right operand of comma operator has no effect [-Wunused-value]
return m[0, 1, 2]; // evaluates as m::operator[2]


Subsequently, regarding

If not 3., then might it make sense to strongly recommending against using operator[], particularly for matrices, possibly to the extent of deprecating it? The current behavior seems like trouble waiting to happen.

In my opinion this is a non-issue, because

  • One can easily check the source code (either the generated Doxygen files, or in this repository) and see that both signatures of Matrix::operator[] accept a single index argument.

  • A reasonably experienced C++ programmer will be aware of the behavior of operator, (and most likely have checked the source code as well).

Spot on regarding the comma operator. Goes all the way back to K&R C.

That probably means we can close this, no?

And not to nitpick, but at the moment the unofficial API docs you've cited are, well, unofficial, and there is still a fair bit more to be done (in terms of breadth of content as well as the actual verbiage used) before they are ready to be promoted as official documentation. Having said that, your feedback is much appreciated and will certainly be taken into account during further development of the documentation.

Thanks for clarifying the parse - apologies, I looked for exactly that.
I understand this particular documentation is unofficial - I'm pointing at in primarily to clarify existing & intended functionality.

Can you comment on the remaining points? In particular, are the following intentional / desirable?

  • operator() checks bounds for 1 arg (Vector), but not 2 args (Matrix).
  • Single-argument versions of operator() and at() are not available for Matrix.
  • At present, no options for Matrix linear-offset-with-bounds-checking (is this right?).

As an end-user, I quite like the the approach used by Armadillo, where the behavior of operator() and at() is consistent over both class and argument number. This would allow somewhat less experienced C++ programmers (or run-of-the-mill R users) to altogether ignore operator[] :)

Was this page helpful?
0 / 5 - 0 ratings