Взятие адреса в C++, так ли это просто?

October 21, 2014

Всем программистам C++ хорошо известно, как получить адрес области памяти, где хранится значение переменной - нужно задействовать оператор взятия адреса (&). Оператор этот унарный. При этом есть и бинарный оператор &, который реализует операцию побитового И. Но нас он совсем не интересует. 

Также, программистам C++ хорошо известно и то, что операторы можно перегружать. И, наверное, хотя бы раз в жизни все пробовали перегрузить для своего собственного типа какой-нибудь оператор (например, [ ]), в надежде, что это упростит работу с данным типом.

Однако не все операторы могут быть перегружены. Например, оператор .  или :: не могут быть перегружены, да и сложно себе представить, чтобы из этого вышло. Представьте, Вы вызвали sizeof для типа T,  и получили в результате 0, потому что кому-то так захотелось. А ведь результат от вызова  sizeof очень часто используется в знаменателе, и никогда не должен быть равен 0.

Но унарный оператор &, оказывается можно перегрузить! Зачем это нужно, кто так делает и как же в этом случае “взять адрес” читайте далее.

Неприятная перегрузка

Приведем код, который лишен практического смысла, но абсолютно “законен” для C++. Объявим класс Foo, у которого перегрузим унарный оператор &:

class Foo
{
public:
    Foo() {}
    int operator &() {
        return  0;
    }
};

Если теперь попытаться узнать адрес объекта типа Foo, то мы всегда будем получать результат - 0.

Foo foo;
std::cout << &foo << std::endl; // Всегда выведет 0

Возникают два вопроса:

  1. когда может понадобиться делать перегрузку такого незыблемого оператора как &;
  2. и как все таки получить адрес объекта, в случае такой перегрузки. 

В интернете есть упоминания о том, как можно реализовать классы-обертки (wrapper), которые как раз задействуют перегрузку оператора &. Не станем давать ссылки на столь вредный материал, но общая идея примерно такая:

template<class T>
class Wrapper
{
public:
    Wrapper(T& obj) : obj_(obj) {}

    T *operator &() {
        return &obj_;
    }

    T const *operator &() const {
        return &obj_;
    }

private:
    T& obj_;
};

Такие перегрузки встречаются в библиотеке ATL (Active Template Library), которая разрабатывалась компанией Microsoft. Вот перечень классов-оберток, перегружающих оператор &: CComTypeAttr, CComVarDesc, CComFuncDesc, CComPtr и так далее. Вот Вам и применение на практике…

Рассмотрим теперь частично реализацию контейнера std::vector из стандартной библиотеки STL. Реализацию вектора возьмем от всё той же компании Microsoft. Вот как выглядит метод push_back:

void push_back(const value_type& _Val)
{   // insert element at end
    if (_Inside(_STD &(_Val)))
    {   // push back an element
        size_type _Idx = _STD &(_Val) - this->_Myfirst;
        
        // Далее код не приводится в целях сохранения простоты примера...
    }
    // Далее код не приводится в целях сохранения простоты примера...
}

Объект, который будет добавлен в контейнер передается по ссылке _Val. Вызывается метод _Inside, который проверяет, “а не содержится ли уже этот объект в контейнере”. Метод _Inside выполняет проверку очень просто - путем сравнения указателей:

bool _Inside(const value_type *_Ptr) const
{   // test if _Ptr points inside vector
    return (_Ptr < this->_Mylast && this->_Myfirst <= _Ptr);
}

Из всего этого Вы должны понять, что стандартная библиотека всегда должна иметь возможность узнать адрес какого-либо объекта. Даже если оператор & будет перегружен у типа. Но как узнать адрес без &.

Решение

Чтобы явно не вызывать & разработчики библиотек прибегали вот к такой жуткой конструкции:

template<class T>
T* addressof(T& arg) 
{
    return reinterpret_cast<T*>(
        &const_cast<char&>(
            reinterpret_cast<const volatile char &>(v)));
}

Шаблонная функция addressof была также реализована в библиотеке boost. А начиная с нового стандарта C++11 addressof “переместился” в стандартную библиотеку. У Microsoft она реализована так (кстати, также она реализована и в GCC):

// TEMPLATE FUNCTION addressof
template<class _Ty> 
inline _Ty *addressof(_Ty& _Val) _NOEXCEPT
{   // return address of _Val
    return (reinterpret_cast<_Ty *>(
        (&const_cast<char&>(
            reinterpret_cast<const volatile char&>(_Val)))));
}

Вернемся к нашему первому примеру с классом Foo, и все-таки получим адрес объекта foo:

Foo foo;
std::cout << std::addressof(foo) << std::endl; // Победа! Адрес получен

std::addressof применяется в STL очень часто, и на то есть причины. Любая библиотека общего назначения, предназначеная для широкого использования, не должна использовать оператор & для получения адреса объектов неизвестного (на этапе компиляции) типа. Для этого нужно использовать std::addressof. Иначе, на такую библиотеку будут наложены ограничения. Ни для boost, ни для stl такие ограничения неприемлемы. Если Вы - разработчик библиотеки, Вам стоит обратить на это внимание. Если же, Вы не разрабатываете библиотек, то Вам стоит “забыть на всегда” о том, что унарный оператор &, может быть перегружен.

Напоследок заметим, что в реализации addressof присутствует reinterpret_cast (даже дважды). Про reinterpret_cast обычно пишут так:

Не портируемо, результат может быть некорректным, никаких проверок не делается.

Но такое приведение типов присутствует в каждой реализации STL, и это обычно не вызывает проблем.