Возврат по ссылке/значению примитивных типов

anton

В этом вопросе сказано, что примитивные типы лучше передавать в функцию по значению. Насколько я понимаю, то же верно и для возврата из функции. Тогда почему операторы индексации (например, вектора) возвращают исключительно ссылки, а не специализированны под возврат по значению для фундаментальных типов? Тем более так наверняка будет использована move семантика.

upd то, что я хочу сделать:

#include <iostream>

template<typename t="">
struct A
{
    T x;

    T& operator[](int) {return x;}
    const T& operator[](int) const {return x;}
};
template<>
struct A<int>
{
    int x;

    int& operator[](int) {return x;}
    int operator[](int) const {return x;}
};


int main()
{
    const A<int> a{};
    A<int> b{};
    //a[0] = 3;
    b[0] = 3;
    std::cout << a[0] << ' ' << b[0];
}
</int></int></int></typename></iostream>
3 ответа

anton

Если специализировать для всех фундаментальных типов каклй-нибудь стандартный контейнер, который обладает множеством различных методов, то это будет очень не эффективно.

Поэтому оператор индексирования в любом случае возвращает ссылку на объект. Передача по ссылке не является ресурсо-затратной операцией. Объекты при этом не создаются и не копируются. Более того для фундаментальных типов конструктор перемещения, как таковой, не существует. Поэтому и нет никакой необходимости специально перегружать стандартные контейнеры для фундаментальных типов, которых немало.

К тому же получится неоднозначность с определением перегруженного оператора индексации, так как встроенный оператор возвращает всегда lvalue.

Есть еще один нюанс. Даже если вы не собираетесь присваивать возвращаемому объекту новое значение, тем не менее у вас может быть необходимость хранить указатели на эти объекты. Например, у вас может возникнуть необходимость создать объект типа std::reference_wrapper допустим с помощью функции std::cref, который будет указывать на исходный объект в контейнере. Если не передавать ссылку на объект из контейнера, то вы не сможете воспользоваться такой возможностью.

Рассмотрите следующий пример

#include <iostream>
#include <functional>
#include <algorithm>

int main()
{
    const std::vector<int> v = { 3, 2, 4, 5, 1 };

    std::vector<std::reference_wrapper<const int="">> rv =
    {
        std::cref(v[0]), std::cref(v[1]),
        std::cref(v[2]), std::cref(v[3]),
        std::cref(v[4])
    };

    std::sort(rv.begin(), rv.end(),
        [](auto &a, auto &b) { return a.get() < b.get(); });

    for (int x : v) std::cout << x << ' ';
    std::cout << std::endl;
    for (const auto &r : rv) std::cout << r.get() << ' ';
    std::cout << std::endl;
}
</std::reference_wrapper<const></int></algorithm></functional></iostream>

Вывод программы на консоль

3 2 4 5 1
1 2 3 4 5

В этой программе объявлен константный вектор. Однако, так как оператор operator [] возвращает ссылку на исходный объект, то можно связать его с объектом std::reference_wrapper, и тем самым представлять элементы исходного вектора упорядочными по различным критериям.

То есть неважно, является ли объект константным или нет, тем не менее нередко необходимо получить на него ссылку, чтобы отслеживать его состояние. Константные объекты могут помимо прочего быть еще и volatile объектами или иметь члены данных с модификатором mutable.

Если же возвращать объект по значению из оператора, то связь с исходным объектом будет утеряна, и вы будете иметь дело с копией.


anton

Думаю, это то, что требуется.

template <class t="">
struct A {
    using ConstIndexRetType = typename std::conditional<std::is_fundamental<t>::value, T, const T&>::type;
    T x;

    T& operator[](int) { return x; }
    ConstIndexRetType operator[](int) const { return x; }
};

int main(int argc, char **argv)
{  
    struct C {};

    const A<int> a;
    const A<c> b;
    static_assert(std::is_same<decltype(a[1]), int="">::value, "?");
    static_assert(std::is_same<decltype(b[1]), const="" c&="">::value, "?");
}
</decltype(b[1]),></decltype(a[1]),></c></int></std::is_fundamental<t></class>


anton

А давайте глянем на практике... Так сказать, найдите три отличия: https://godbolt.org/g/s95gJt

Вот это

void f(const A<int>& a, A<int>& b)
{
    std::cout << a[0];
    std::cout << b[0];
}
</int></int>

компилируется в

push    rbx
    mov     rbx, rsi
    mov     esi, DWORD PTR [rdi]
    mov     edi, OFFSET FLAT:std::cout
    call    std::basic_ostream<char, std::char_traits<char=""> >::operator<<(int)
    mov     esi, DWORD PTR [rbx]
    mov     edi, OFFSET FLAT:std::cout
    pop     rbx
    jmp     std::basic_ostream<char, std::char_traits<char=""> >::operator<<(int)
</char,></char,>

Как видите, разницы никакой.

licensed under cc by-sa 3.0 with attribution.