Доступ к приватным полям через арифметику указателей?

Текст задания: все поля этого класса закрытые, ваша задача реализовать несколько функций, которые дают полный доступ к этим полям (см. шаблон кода), несмотря на то, что они закрытые.
/*
 * Класс Cls определен точно таким образом:
 *
 * struct Cls {
 * Cls(char c, double d, int i);
 * private:
 *     char c;
 *     double d;
 *     int i;
 * };
 *
 */

// Эта функция должна предоставить доступ к полю c объекта cls.
// Обратите внимание, что возвращается ссылка на char, т. е.
// доступ предоставляется на чтение и запись.
char &get_c(Cls &cls) {
    char* ptr;
    ptr = (char*)&cls;
    return *ptr;
}

// Эта функция должна предоставить доступ к полю d объекта cls.
// Обратите внимание, что возвращается ссылка на double, т. е.
// доступ предоставляется на чтение и запись.
double &get_d(Cls &cls) {
    double* ptr;
    ptr = (double*)( (char*)(&cls) ) + 1;
    return *ptr; 
}

// Эта функция должна предоставить доступ к полю i объекта cls.
// Обратите внимание, что возвращается ссылка на int, т. е.
// доступ предоставляется на чтение и запись.
int &get_i(Cls &cls) {
    double* p;
    int* ptr;
    p = (double*)( (char*)(&cls) ) + 1;
    ptr = (int*) (p + 1);
    
    return *ptr;
}


Можете, пожалуйста, объяснить как точно все это устроено в памяти?
Ясен момент, когда мы получаем поле char c; Преобразуем ссылку на объект как указатель типа char.
Однако затем не совсем ясно, что и почему происходит. Что нам дает преобразование (double*)( (char*)(&cls) ) + 1? Ведь char занимает 1 байт, а мы преобразуем его в double. Понятно, что нам надо взять после char 8 байт, чтобы получить поле double.
Еще больше вопросов вызывает получение поля типа int. Например, если ptr = (int*) (p) + 1, то это приведет к неверному результату. Почему так происходит?
  • Вопрос задан
  • 667 просмотров
Решения вопроса 1
jcmvbkbc
@jcmvbkbc
"I'm here to consult you" © Dogbert
Можете, пожалуйста, объяснить как точно все это устроено в памяти?

Обычно это устроено так, что поля идут одно за другим в памяти. Но кроме размера у полей есть выравнивание. Например, uint32_t выравнивается на 4 байта, а uint64_t -- на 8. Поэтому между идущими подряд полями разного типа могут быть дырки.
В приведённом примере double -- поле с наибольшим выравниванием, выравнивание объекта будет на 8, поле c будет по смещению 0 в объекте, поле d -- по смещению 8, а поле i -- по смещению 16. Если иметь это в виду, то игры с указателями приобретают смысл.

Текст задания: все поля этого класса закрытые, ваша задача реализовать несколько функций, которые дают полный доступ к этим полям (см. шаблон кода), несмотря на то, что они закрытые.

Пожалуйста, никогда так не делай.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 3
mayton2019
@mayton2019
Bigdata Engineer
Формально, с точки зрения ООП так делать нельзя. Я не знаю продуктовых задач где такой злостный хак имел бы место.

А преподавателю который это придумал - надо оторвать яйца.
Ответ написан
@k-morozov
если нет виртуальных методов, то можно считать что данные идут подряд. по сути тебе нужно правильно обратиться к нужному участку памяти. Почитай про выравнивание в памяти.
Ответ написан
Комментировать
@oleghab
В реальности если требуется обойти ограничение доступа, то это может быть сделано например так:
/*
 * Класс Cls определен точно таким образом:
 *
 * struct Cls {
 * Cls(char c, double d, int i);
 * private:
 *     char c;
 *     double d;
 *     int i;
 * };
 *
 */

namespace {
    // Cls_Double - точная копия Cls в смысле расположения данных.
    // Важный момент - данные в Cls НЕ скрыты, но видимы с ограничением доступа (что разные вещи)
    // Это позволяет делать дубликат класса всегда
    struct Cls_Double {
        char c;
        double d;
        int i;
    };
}

// Эта функция должна предоставить доступ к полю c объекта cls.
// Обратите внимание, что возвращается ссылка на char, т. е.
// доступ предоставляется на чтение и запись.
char &get_c(Cls &cls) {
    return reinterpret_cast<Cls_Double&>(cls).c;
}

// Эта функция должна предоставить доступ к полю d объекта cls.
// Обратите внимание, что возвращается ссылка на double, т. е.
// доступ предоставляется на чтение и запись.
double &get_d(Cls &cls) {
    return reinterpret_cast<Cls_Double&>(cls).d;
}

// Эта функция должна предоставить доступ к полю i объекта cls.
// Обратите внимание, что возвращается ссылка на int, т. е.
// доступ предоставляется на чтение и запись.
int &get_i(Cls &cls) {
    return reinterpret_cast<Cls_Double&>(cls).i;
}


Пара замечаний:
1) приведение через арифметику указателей и reinterpret_cast оба нарушают strict aliasing, поэтому без разницы что использовать
2) если есть внутренние терзания, то для хотя бы относительного душевного спокойствия можно использовать двойной static_cast вместо reinterpret_cast, тоже нарушит strict aliasing, так что всё равно
3) Возможно, если это академическое задание, ожидается ручное вычисление через арифметику указателей, в стиле
return *(char *)((std::uint8_t*)&cls);
return *(double *)((std::uint8_t*)&cls + sizeof(char));
return *(int *)((std::uint8_t*)&cls + sizeof(char) + sizeof(double));


Но никогда так не делайте в проде, потому как можно заиметь кучу проблем с поддежкой таких вычислений, в том числе если вдруг Cls на самом деле будет наследоваться от класса с vptr, который при этом снаружи может измениться. Тогда Cls_Double нужно идентично наследовать, а компилятор обеспечит корректные адресации.
Ещё один вариант - вопросы упаковки данных, который тоже компилятор может бесплатно решить за нас.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы