Как получить доступ к приватному члену класса?

Рейтинг: 7Ответов: 3Опубликовано: 08.06.2023

На сайте с вопросами с интервью есть следующий вопрос:

class Something {
public:
    Something() {
        topSecretValue = 42;
    }
    bool somePublicBool;
    int somePublicInt;
    std::string somePublicString;
private:
    int topSecretValue;
};

Необходимо [не трогая исходный класс] получить значение topSecretValue. И приводится решение:

class SomethingReplica {
public:
    int getTopSecretValue() { return topSecretValue; }
    bool somePublicBool;
    int somePublicInt;
    std::string somePublicString;
private:
    int topSecretValue;
};

int main(int argc, const char * argv[]) {
    Something a;
    SomethingReplica* b = reinterpret_cast<SomethingReplica*>(&a);
    std::cout << b->getTopSecretValue();
}

Мой вопрос в том, является ли такое решение валидным? Разве мы можем кастить к произвольным классам? И если нет, то есть ли у этого вопроса валидное решение?

Ответы

▲ 6Принят

№0: Дратути

Используется отсутствие проверки доступа при инстанцировании шаблона.

template<typename x_Ptr>
struct Get
{
    static inline x_Ptr ptr;
};

template<auto x_ptr>
struct Access
{
    static inline int dummy{(Get<decltype(x_ptr)>::ptr = x_ptr, 0)};
};

#include <string>
#include <iostream>

// класс без изменений!
class Something {
public:
    Something() {
        topSecretValue = 42;
    }
    bool somePublicBool;
    int somePublicInt;
    std::string somePublicString;
private:
    int topSecretValue;
};

template struct Access<&Something::topSecretValue>;

int main()
{
    Something something{};
    ::std::cout << something.*(Get<int Something::*>::ptr);
}

online compiler

▲ 5

Вот, из Саттера — всевозможные на тот момент С++03 способы :)

Рассмотрим следующий заголовочный файл.

// Файл x.h 
// 
class X { 
public:
    X() : private_(1) { /*...*/ }
    template<class T>
    void f( const T& t ) { /*...*/ }
    int Value() { return private_; }
    // ...
private: 
    int private_; 
};

Покажите, как произвольный вызывающий код может получить непосредственный доступ к закрытому члену класса private_.

Преступник №1: фальсификатор

Метод фальсификатора состоит в том, чтобы продублировать определение фальсифицируемого класса, в которое внесены все необходимые нам изменения, например:

// Пример 15-1: ложь и подделка
//
class X {
    // Вместо включения файла x.h, вручную (и незаконно)
    // дублируем определение X и добавляем строку наподобие
    // этой: 
    friend ::Hijack( X& );
};
void Hijack( X& x ) {
    x.private_ = 2;      // Злобный смех
}

Конечно, то, что сделано, — не законно. Это не законно хотя бы потому, что нарушает правило одного определения, которое гласит, что если тип (в нашем случае — X) определен более одного раза, то все его определения должны быть идентичны. Используемый же в приведенном примере объект хотя и называется X и выглядит похожим на X, но это не тот же X, который используется остальным кодом программы. Тем не менее, такой “хак” будет работать на большинстве компиляторов, поскольку расположение данных объекта останется неизменным.

Преступник №2: карманник

Карманник сработает тихо — подменив смысл определения класса, например, следующим образом.

// Пример 15-2: использование макромагии
//
#define private public        // Не законно!
#include "x.h"
void Hijack( X& x ) {
    x.private_ = 2;           // Злобный смех
}

Этот человек — карманник. Запомните его умелые пальцы!
Конечно, то, что делает карманник, — не законно. Код из примера 15-2 непереносим по двум причинам.
 Не законно переопределять при помощи директивы #define зарезервированные слова.
 При этом происходит такое же нарушение правила одного определения, как и у фальсификатора. Однако если расположение данных объекта остается неизменным, этот способ сработает.

Преступник №3: мошенник

Принцип работы мошенника — в подмене понятий.

// Пример 15-3: попытка имитации размещения данных объекта
//
class BaitAndSwitch {// В надежде, что размещение данных в
public:              // этом классе будет тем же, что и в X
    int notSoPrivate;
};
void f( X& x ) {
    (reinterpret_cast<BaitAndSwitch&>(x)).notSoPrivate = 2;
}                                           // Злобный смех

Этот человек — мошенник. Хорошенько его запомните! Его реклама рассчитана только на то, чтоб затащить вас в магазин, а уж там он обязательно ухитриться продать вам совершенно не ту вещь, о которой шла речь в рекламе, да еще и в несколько раз дороже, чем в любом другом месте.

Конечно, то, что делает мошенник, — не законно. Код из примера 15-3 не законен по двум причинам.
 Нет гарантии, что размещение объектов X и BaitAndSwitch в памяти будет одинаковым — хотя на практике это обычно так и есть.
 Результат reinterpret_cast не определен, хотя большинство компиляторов сделают именно то, чего от них хочет мошенник. В конце концов, говоря волшебное слово reinterpret_cast, вы заставляете компилятор поверить вам и закрыть глаза на подготавливаемое вами мошенничество.

Персона грата №4: адвокат

Многие из нас недаром больше боятся хорошо одетых и улыбающихся адвокатов, чем (других) преступников.
Рассмотрим следующий код.

// Пример 15-4: проныра-законник
//
namespace {
    struct Y {};
}
template<>
void X::f( const Y& ) {
    private_ = 2;               // Злобный смех
}
void Test() {
    X x;
    cout << x.Value() << endl;  // Выводит 1
    x.f( Y() );
    cout << x.Value() << endl;  // Выводит 2
}

Этот человек — адвокат, который знает все лазейки. Его невозможно поймать, поскольку он слишком осторожен, чтобы нарушить букву закона, при этом нарушая его дух. Запомните и избегайте таких неджентльменов.
Как бы мне ни хотелось сказать “Конечно, то, что делает адвокат, — не законно”, увы, я не могу этого сделать, поскольку все сделанное в последнем примере законно. Почему? В примере 15-4 использован тот факт, что у X есть шаблон функции-члена. Приведенный код соответствует стандарту, так что последний гарантирует, что он будет работать так, как ожидается.

▲ 3

Мы в продакшене (только тсс!) делаем примерно вот так.

Это похоже на ответ @user7860670, но работает полностью во время компиляции (можно использовать в constexpr-выражениях), и не плодит лишних переменных в рантайме.

Использование:

#include <iostream>

class A
{
    int a;

  public:
    constexpr A(int a) : a(a) {}
};

template struct RegisterMember<"my_a", &A::a>;

int main()
{
    A a(42);
    std::cout << GetMember<"my_a">(a) << '\n';
}

Реализация:

#include <algorithm>
#include <cstddef>
#include <functional>

template <std::size_t N>
struct ConstString
{
    char value[N]{};

    constexpr ConstString() {}
    constexpr ConstString(const char (&source)[N])
    {
        std::copy_n(source, N, value);
    }
};

namespace // To avoid ODR violations for duplicate keys.
{
    namespace detail::PrivateAccess
    {
        [[maybe_unused]] constexpr void _adl_GetMember() {} // Dummy ADL target.

        template <ConstString Key>
        struct Reader
        {
            #if defined(__GNUC__) && !defined(__clang__)
            #pragma GCC diagnostic push
            #pragma GCC diagnostic ignored "-Wnon-template-friend"
            #endif
            friend constexpr auto _adl_GetMember(Reader<Key>);
            #if defined(__GNUC__) && !defined(__clang__)
            #pragma GCC diagnostic pop
            #endif
        };

        template <ConstString Key, auto Value>
        struct Writer
        {
            friend constexpr auto _adl_GetMember(Reader<Key>)
            {
                return Value;
            }
        };
    }

    template <ConstString Key, typename T, typename ...P>
    [[nodiscard]] constexpr decltype(auto) GetMember(T &&target, P &&... params)
    {
        using detail::PrivateAccess::_adl_GetMember;
        return std::invoke(_adl_GetMember(detail::PrivateAccess::Reader<Key>{}), std::forward<T>(target), std::forward<P>(params)...);
    }
}

template <ConstString Key, auto Value>
struct RegisterMember : detail::PrivateAccess::Writer<Key, Value> {};