Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Square and round brackets operator, how to choose overloads?

I want to access to some class data using operator[] but depending on the index type into the square brackets return one kind of data or other. As a simplified example:

struct S
{
    int   &operator []( int index ) { std::cout << "[i]"; return i_buffer[index]; }
    short &operator [](short index) { std::cout << "[s]"; return s_buffer[index]; }

private:
    int   i_buffer[10]{ 0,  1,   2,   3,   4,   5,   6,   7,   8,   9  };
    short s_buffer[10]{ 0, 111, 222, 333, 444, 555, 666, 777, 888, 999 };
};

There's no way to write a short literal, so the only way to choose the short overload is by casting:

S s;
std::cout << s[9] << '\n';        // prints [i]9
std::cout << s[(short)9] << '\n'; // prints [s]999

But I don't like it and I was wondering if there's different options.

What I've tried?

Tagged parameter.

First I've tried to use "tags":

struct S
{
    enum class i_type : std::int32_t {};
    enum class s_type : std::int32_t {};

    int   &operator [](i_type index)
    { std::cout << "[i]"; return i_buffer[static_cast<int>(index)]; }
    short &operator [](s_type index)
    { std::cout << "[s]"; return s_buffer[static_cast<int>(index)]; }

private:
    int   i_buffer[10]{ 0,  1,   2,   3,   4,   5,   6,   7,   8,   9  };
    short s_buffer[10]{ 0, 111, 222, 333, 444, 555, 666, 777, 888, 999 };
};

That works but is still a little verbose:

S s;
std::cout << s[9] << '\n';            // error, no possible overload to be taken
std::cout << s[S::i_type{9}] << '\n'; // prints [i]9
std::cout << s[S::s_type{9}] << '\n'; // prints [s]999

Template.

As a crazy workaround I wanted to try to template the operator:

struct S
{
    template <typename T>
    T &operator [](T) { std::cout << "???"; return 0; }

private:
    int   i_buffer[10]{ 0,  1,   2,   3,   4,   5,   6,   7,   8,   9  };
    short s_buffer[10]{ 0, 111, 222, 333, 444, 555, 666, 777, 888, 999 };
};

template <>
int   &S::operator [](int index)   { std::cout << "[i]"; return i_buffer[index]; }
template <>
short &S::operator [](short index) { std::cout << "[s]"; return s_buffer[index]; }

The template version behaves as the original code, but there's no easy way to specify a type parameter along with the operator[]:

S s;
std::cout << s[9] << '\n';        // prints [i]9 like before
std::cout << s[(short)9] << '\n'; // prints [s]999 like before
std::cout << s<short>[9] << '\n'; // s is not template
std::cout << s[9]<short> << '\n'; // nonsense
// Correct but utterly verbose and hard to write and read
std::cout << s.operator[]<short>(9) << '\n';

Question.

All the issues described also happens with operator(), I want to know if there's more alternatives that I'm not aware of?

like image 583
PaperBirdMaster Avatar asked Jan 25 '17 10:01

PaperBirdMaster


People also ask

What's the difference between square brackets and round brackets?

Usually we use square brackets - [ ] - for special purposes such as in technical manuals. Round brackets - ( ) - are used in a similar way to commas when we want to add further explanation, an afterthought, or comment that is to do with our main line of thought but distinct from it.

What is the name of [] operator?

Overloading Subscript or array index operator [] in C++ This is a binary or n-ary operator and is represented in two parts: postfix/primary expression. expression.

What does square brackets in r mean?

The square brackets are used for indexing into a vector, matrix, array, list or dataframe. You can think of the square brackets as marking the edges of a cell, column or row of a table. The square brackets are also called extraction operators as they are used to extract specific elements from a vector or matrix.

What is bracket operator?

Many people probably use brackets ([]) in their code without realizing that this is an operator. Mostly, it is seen as a way to get hold of one element in an array. In fact, it is an operator, although the notation is a bit unlike that of most operators.


3 Answers

I think that using a named method is a much better idea than using operator[] in your situation, as it would be easier to understand that two separate buffers are being accessed by reading the source code.

Regardless, if you want to use your operator[] approach, you could use strong typedefs and user defined literals to have type-safety with minimal syntactic overhead:

BOOST_STRONG_TYPEDEF(std::size_t, int_index)
BOOST_STRONG_TYPEDEF(std::size_t, short_index)

struct S
{
    auto& operator[](int_index i) { /* ... */ }
    auto& operator[](short_index i) { /* ... */ }
};

auto operator "" _ii(unsigned long long int x) { return int_index{x}; }
auto operator "" _si(unsigned long long int x) { return short_index{x}; }

You can then call your methods as follows:

S s;

auto& some_int = s[15_ii];
auto& some_short = s[4_si];

wandbox example

like image 65
Vittorio Romeo Avatar answered Nov 03 '22 04:11

Vittorio Romeo


I think I'd use std::tie from the <tuple> library and then write a little helper to find the correct reference type:

#include <tuple>
#include <iostream>

template<class As, class...Ts>
auto& as(std::tuple<const Ts&...>ts)
{
    return std::get<As const&>(ts);
};

template<class As, class...Ts>
auto& as(std::tuple<Ts&...>ts)
{
    return std::get<As &>(ts);
};

struct S
{
    // both cost and mutable version provided for completeness.

    auto operator[](std::size_t i) const {
        return std::tie(i_buffer[i], s_buffer[i]);
    }

    auto operator[](std::size_t i) {
        return std::tie(i_buffer[i], s_buffer[i]);
    }

private:
    int   i_buffer[10]{ 0,  1,   2,   3,   4,   5,   6,   7,   8,   9  };
    short s_buffer[10]{ 0, 111, 222, 333, 444, 555, 666, 777, 888, 999 };
};

int main()
{
    auto s = S();
    const auto x = S();

    std::cout << "short is : " << as<short>(s[5])<< '\n';
    std::cout << "int is : " << as<int>(s[5])<< '\n';

    std::cout << "short is : " << as<short>(x[6])<< '\n';
    std::cout << "int is : " << as<int>(x[6])<< '\n';
}

This way, the code is explicit but still succinct.

expected output:

short is : 555
int is : 5
short is : 666
int is : 6

Having read the further comments, I might choose to store the matrix in (say) row-wise form and then provide a col-wise wrapper.

A barely functional example:

#include <tuple>
#include <iostream>
#include <array>


template<std::size_t Rows, std::size_t Cols>
struct RowWiseMatrix
{

    auto& operator[](std::size_t i) { return data_[i]; }

    std::array<std::array<double, Cols>, Rows> data_;
};

template<std::size_t Rows, std::size_t Cols>
struct ColumnProxy
{
    ColumnProxy(std::array<std::array<double, Cols>, Rows>& data, std::size_t col)
            : data_(data), col_(col)
    {

    }

    auto& operator[](std::size_t i) { return data_[i][col_]; }

    std::array<std::array<double, Cols>, Rows>& data_;
    std::size_t col_;
};


template<std::size_t Rows, std::size_t Cols>
struct ColWiseProxy
{
    ColWiseProxy(RowWiseMatrix<Rows, Cols>& mat) : underlying_(mat) {}

    auto operator[](std::size_t i) { return ColumnProxy<Rows, Cols> { underlying_.data_, i }; }

    RowWiseMatrix<Rows, Cols>& underlying_;
};


template<std::size_t Rows, std::size_t Cols>
auto& rowWise(RowWiseMatrix<Rows, Cols>& mat)
{
    return mat;
};

template<std::size_t Rows, std::size_t Cols>
auto colWise(RowWiseMatrix<Rows, Cols>& mat)
{
    return ColWiseProxy<Rows, Cols>(mat);
};

int main()
{
    auto m = RowWiseMatrix<3, 3> {
            std::array<double, 3>{ 1, 2, 3 },
            std::array<double, 3>{ 4, 5, 6},
            std::array<double, 3>{ 7, 8, 9}
    };

    std::cout << rowWise(m)[0][2] << '\n';
    std::cout << colWise(m)[0][2] << '\n';
}

Expected output:

3
7
like image 21
Richard Hodges Avatar answered Nov 03 '22 05:11

Richard Hodges


I agree with Vittorio Romeo that the best solution is a named method.

However here is a solution:

template <class T> struct S_proxy {
  T* data; 
  T& operator[](std::size_t i) { return data[i]; }
};

struct S
{
    auto i_type() { return S_proxy<int>{i_buffer}; };
    auto s_type() { return S_proxy<short>{s_buffer}; };

private:
    int   i_buffer[10]{ 0,  1,   2,   3,   4,   5,   6,   7,   8,   9  };
    short s_buffer[10]{ 0, 111, 222, 333, 444, 555, 666, 777, 888, 999 };
};

and use:

S s;
return s.s_type()[2];
like image 1
bolov Avatar answered Nov 03 '22 05:11

bolov