13

Есть учебное задание, в котором необходимо получить доступ к private-полям класса извне. struct Cls дана изначально. Я нагуглил что нужно создать копию структуры, но с public методами, а потом через нее стучаться в изначальную. Вопрос в том как это реализовать? Как можно вернуть ссылку на private поля?

struct Cls {
  Cls(char c, double d, int i);
  private:
      char c;
      double d;
      int i;
  };



 struct B {
  B(char c, double d, int i);
  public:
      char c1;
      double d1;
      int i1;
 };

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

    return ((B*)(&cls))->c1 = 'p';

}

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

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

int main() {
    Cls cls('h', 2.0, 3);
    char ch = get_c(&cls);
    cout << ch << endl;
}
VladD
  • 206,799
advortsov
  • 560
  • 3
  • 8
  • 17
  • Это где ж такие учебные задания? Что Вам непонятно в приведенном коде? –  Oct 01 '15 at 13:28
  • мне непонятно как получить ссылку на прайвэт поля первого класса, использую второй – advortsov Oct 01 '15 at 13:43
  • 4
    #define private public – Sergey S Oct 01 '15 at 14:02
  • @zenden2k: UB в чистом виде. – VladD Oct 01 '15 at 14:20
  • @VladD, если весь код в одном файле, то точно никакого UB. Update: или это не к defiine'у относилось? – Qwertiy Oct 04 '15 at 17:05
  • Пора бы принять какой-нибудь ответ. – Qwertiy Oct 04 '15 at 17:08
  • @VladD, препроцессор подставляется до компиляции. Если получившийся код корректен, то ни о каком UB не может быть и речи. Тут тогда явно impementation defined - либо препроцессор компилятора съел этот define, либо нет. О UB можно было бы говорить, если бы мы для заголовочного файла поменяли private на public, а в cpp-файле оставили private (кстати, не уверен, насколько вообще такой финт пройдёт). – Qwertiy Oct 04 '15 at 19:07
  • @Qwertiy: Вы говорите о какой-то конкретной имплементации препроцессора. А стандарт запрещает подобные трюки. – VladD Oct 04 '15 at 19:58
  • @Vlad. Если компилятор после препроцессора получил код, который соответствует стандарту и сам по себе не содержит UB, то как можно говорить о UB из-за препроцессора? Тут вариантов всего три: 1. define сработал, компилятор получил код без UB и UB нет; 2. define не сработал, компилятор получил корректный код без UB, без подстановки макроса - UB всё равно бы не было, но в нашем случае это ошибка компиляции из-за private; 3. некорректный define был замечен и компиляция отменена. Ни в одном случае получить UB нельзя. Я говорю, что это нельзя назвать UB. Я НЕ говорю, что стандарт это разрешает. – Qwertiy Oct 04 '15 at 20:07
  • @Qwertiy: Поведение препроцессора тоже описывается стандартом. – VladD Oct 04 '15 at 20:26
  • @VladD, а разве я с этим спорил? – Qwertiy Oct 05 '15 at 07:25

8 Answers8

12

Вы не можете честным и надёжным путём получить доступ к приватным данным. Существуют грубые хаки, наподобие «угадать бинарный лэйаут данных и скастить указатель», которые прямо запрещены стандартом, и дают право компилятору наказать вас в любой момент.

Правильный ответ на вопрос, как получить доступ к приватным данным — никак. Ваш преподаватель либо задаёт вопрос с подвохом, либо плохо знает язык, который преподаёт.

Конкретно в вашем случае, код

(B*)(&cls)->c1

нарушает strict aliasing rule (правило 3.10/10 стандарта).

Дополнительное чтение по теме:


Окей, исходя из развернувшейся дискуссии в ответе @Vlad from Moscow, вопрос о доступе через указатель на «чужой» тип не так уж очевиден даже из стандарта. Как видите, мы покамест не пришли к общему мнению о том, правомерен ли такой доступ по стандарту. В любом случае, доступ к приватным полям — очень плохой стиль программирования, и даже если так возможно сделать, делать этого не нужно.


Обновление: Другие ответы: ([1], [2] и [3]) убедили меня в том, что к приватным полям таки можно получить доступ «законным» — то есть, совместимым со стандартом путём. (Впрочем, с трактовкой стандарта в последнем из них я не вполне согласен, но это лишь показывает, что сам по себе стандарт — достаточно большой и не очень ясно написанный текст.)

Тем не менее, любой из приведённых подходов кажется мне грубым хаком, и я бы крайне не рекомендовал пользоваться ими в production-коде. Думаю, будет поучительно, если вы отправите ссылку на это обсуждение вашему преподавателю.

VladD
  • 206,799
  • В приведении указателя к другому типу нет UB. Оно есть в перемешанных обращениях к одним данным через указатели разных типов. В данном случае смешения нет, т. к. первое обращение по указателю нового типа (и его присваивание) делается после последнего по указателю старого. Правило отсутствие смешанных обращений направлено на кэширование значений - если ты меняешь через другой тип, то там значение может взяться из кэшированной копии и оказаться старым. И то компиляторы типа clang'а особо осторожно работают с явными кастами. В данном коде ничего подобного нет, т. е. он корректен и не содержит UB. – Qwertiy Oct 01 '15 at 16:48
  • @Qwertiy: Мне кажется, что стандарт с вами не согласен. Насколько я понимаю стандарт, любое обращение к объекту через указатель несовместимого типа — UB. – VladD Oct 01 '15 at 16:50
  • Кстати, ещё одна мысль. Если в структуре поле типа double и я обращаюсь к нему через указатель на тип double, то нарушения тоже нет, независимо от того, каким способом я получил этот указатель - хоть с клавиатуры ввёл. – Qwertiy Oct 01 '15 at 16:50
  • @Qwertiy: Это да, но проблема не в этом, а в ->, стрелка вычисляет оффсет. В любом случае, если у вас найдётся ссылка на нужный пункт в стандарте, будет круто. – VladD Oct 01 '15 at 16:51
  • 1
    Похоже, я не прав. http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html (доступна страница в кэше яндекса, оригинал у меня не открывается) Violating Type Rules: It is undefined behavior to cast an int* to a float* and dereference it (accessing the "int" as if it were a "float"). C requires that these sorts of type conversions happen through memcpy: using pointer casts is not correct and undefined behavior results. Но в любом случае, каст корректен, некорректно именно разыменование. Кстати, берём две структуры, делаем memcpy и получаем корректный код - это там явно написано. – Qwertiy Oct 01 '15 at 16:57
  • @Qwertiy: Угу, проблема именно в разыменовании. – VladD Oct 01 '15 at 17:05
  • Но если мы сделали каст (разыменования нет), добыли адрес нужного поля (теперь тип совпадает) и разыменовали его, то UB нет? – Qwertiy Oct 01 '15 at 17:10
  • @Qwertiy: Хм, а как мы добыли адрес? С чужим типом это вроде бы нельзя? – VladD Oct 01 '15 at 17:13
  • Получить для совместимого типа offsetof и сложить с void* годится? – Qwertiy Oct 01 '15 at 17:16
  • @Qwertiy: Хм. Если есть гарантия, что оффсеты совпадают, то может и сработать даже по стандарту. По-моему, хорошая идея, катит на отдельный ответ. – VladD Oct 01 '15 at 17:17
  • Ага, я как раз код дописал :) – Qwertiy Oct 01 '15 at 17:22
9

Формально можно обмануть компилятор следующим образом

#include <iostream>

struct Cls {
  Cls(char c, double d, int i) : c( c ), d( d ), i( i ) {}
  private:
      char c;
      double d;
      int i;
  };



 struct B {
  B(char c, double d, int i) : c1( c ), d1( d ), i1( i ) {}
  public:
      char c1;
      double d1;
      int i1;
 };

char &get_c( Cls &cls ) 
{
    void *p = &cls;

    B *pb = static_cast<B *>( p );

    return pb->c1 = 'A';;
}


int main()
{
    Cls cls('h', 2.0, 3);
    char ch = get_c( cls) ;
    std::cout << ch << std::endl;

}    

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

A

Это совершенно корректный код, так как обе структуры являются layout-compatible и согласно стандарту C++ (3.9.2 Compound types)

  1. ...Pointers to cv-qualified and cv-unqualified versions (3.9.3) of layout-compatible types shall have the same value representation and alignment requirements (3.11).

Более подробно: 9.2 Class members:

16 Two standard-layout struct (Clause 9) types are layout-compatible if they have the same number of non-static data members and corresponding non-static data members (in declaration order) have layout-compatible types (3.9).

и 9 Classes:

7 A standard-layout class is a class that:

— has no non-static data members of type non-standard-layout class (or array of such types) or reference,

— has no virtual functions (10.3) and no virtual base classes (10.1),

— has the same access control (Clause 11) for all non-static data members,

— has no non-standard-layout base classes,

— either has no non-static data members in the most derived class and at most one base class with non-static data members, or has no base classes with non-static data members, and — has no base classes of the same type as the first non-static data member.

8 A standard-layout struct is a standard-layout class defined with the class-key struct or the class-key class. A standard-layout union is a standard-layout class defined with the class-key union

Вы также в main могли бы объявить ссылку на объект типа char. Вот более наглядная программа благодаря добавлению дружественного оператора вывода

#include <iostream>

struct Cls {
  Cls(char c, double d, int i) : c( c ), d( d ), i( i ) {}
  private:
      char c;
      double d;
      int i;
    friend std::ostream & operator << ( std::ostream &os, const Cls &cls )
    {
        return os << "c = " << cls.c << ", d = " << cls.d << ", i = " << cls.i;
    }        
  };



 struct B {
  B(char c, double d, int i) : c1( c ), d1( d ), i1( i ) {}
  public:
      char c1;
      double d1;
      int i1;
 };

char &get_c( Cls &cls ) 
{
    void *p = &cls;

    B *pb = static_cast<B *>( p );

    return pb->c1 = 'A';;
}


int main()
{
    Cls cls('h', 2.0, 3);
    char &ch = get_c( cls) ;
    std::cout << ch << std::endl;
    ch = 'B';

    std::cout << cls << std::endl;
Ъ

Ее вывод на консоль:

A
c = B, d = 2, i = 3
  • Эээ... pointer aliasing? – VladD Oct 01 '15 at 13:51
  • @VladD Смотрите мой обновленный ответ. – Vlad from Moscow Oct 01 '15 at 14:14
  • Хм. Всю жизнь думал, что нельзя. Перечитаю стандарт. – VladD Oct 01 '15 at 14:16
  • Хм, всё ещё не понимаю. Для начала, почему эти типы layout compatible? – VladD Oct 01 '15 at 14:22
  • has the same access control (Clause 11) for all non-static data members — это вроде бы не выполняется – VladD Oct 01 '15 at 14:27
  • @VladD Это имеется в виду, что внутри одной структуры все нестатические члены имеют тот же самый класс доступа. – Vlad from Moscow Oct 01 '15 at 14:28
  • А, понял. Хм, неужели оставлена такая дыра? :-\ Сейчас перепроверю. – VladD Oct 01 '15 at 14:28
  • @VladD Это сделано для связи с другими языками программирования. – Vlad from Moscow Oct 01 '15 at 14:30
  • Окей, я согласен, что данные типы layout-compatible. Но почему при этом катит каст через void*? В 3.10/10 нет ни слова насчёт layout-compatible, а значит, такой доступ вроде бы всё равно неправомерен. – VladD Oct 01 '15 at 14:33
  • @VladD Указатели имеют то же самое представление и выравнивание. Вы не можете напрямую делать приведение типов, так как нет такого преобразования. Но вы можете это делать через void указатель. – Vlad from Moscow Oct 01 '15 at 14:36
  • Но не является ли это UB? Имеет ли компилятор право увидеть, что во всей программе нет ни одного объекта типа B, и выкинуть всю процедуру, т. к. не может быть законного обращения к памяти по указателю типа B*? То же представление — да, но этого ведь не достаточно? Если идти по дороге UB, можно тогда уж и просто вручную воспользоваться #define private public #define class struct – VladD Oct 01 '15 at 14:38
  • @VladD Например, функция malloc возвращает void указатель, не так ли, которые вы преобразуете к указателю требуемого типа. – Vlad from Moscow Oct 01 '15 at 14:40
  • @VladD конструктор лишь инициализирует члены класса с фундаментальными типами. – Vlad from Moscow Oct 01 '15 at 14:42
  • Ну, с malloc проблем нет — до того, как вы сконструируете объект в выделенной памяти, вы не имеете права обращаться к этой памяти. Указатель держать можно, разыменовывать нельзя, если по адресу реально нет объекта нужного типа. – VladD Oct 01 '15 at 14:42
  • @VladD Реально объект существует. Он реинтерпретируется как другая структура. – Vlad from Moscow Oct 01 '15 at 14:44
  • ... А реинтерпретация — не противоречит ли она 3.10/10? – VladD Oct 01 '15 at 14:47
  • 1
    @VladD Вы этом состоит смысл layout-compatible, что вы можете обращаться к структурам, которые были определены в других языках программирования. – Vlad from Moscow Oct 01 '15 at 14:56
  • 1
    @VladD Я только что задал соответствующий вопрос на форуме по обсуждению стандарта C++. Так что подождем, что скажут в этом отношении специалисты со всего мира.:) Я сообщу о результатах. – Vlad from Moscow Oct 01 '15 at 15:10
  • Ого! Круто! Ждём. – VladD Oct 01 '15 at 15:54
  • А можно ссылку? Интересно почитать, что пишут гуру. – VladD Oct 01 '15 at 16:02
  • @VladD Попробуйте зайти по этой ссылке https://groups.google.com/a/isocpp.org/forum/#!forum/std-discussion – Vlad from Moscow Oct 01 '15 at 16:06
  • Ага, открылось, спасибо! (Почему-то не с первого раза.) – VladD Oct 01 '15 at 16:11
  • return pb->c1 = 'A';;

    не понял эту строчку? зачем return 'A' делать?

    – advortsov Oct 01 '15 at 17:06
  • 1
    @Александр Дворцов У вас же в исходном примере стоит следующее предложение return ((B*)(&cls))->c1 = 'p'; Я просто заменил 'p' нв 'A' для наглядности и возвратил ссылку на этот объект, которому только что было присвоено новое значение. – Vlad from Moscow Oct 01 '15 at 17:44
8

Легальный способ - это явная инстанциация шаблона.
Согласно [temp.spec] p6:

Правила доступа не применяются к именам использованным при явных инстанциациях. [ Примечание: в частности, шаблонные аргументы и имена использованные при декларации функции (...) могут быть приватными типами или членами, которые обычно не были бы доступны, и шаблон может быть шаблоном функции-члена класса, которая обычно не была бы доступна. — конец примечания ]

(В оригинале: The usual access checking rules do not apply to names in a declaration of an explicit instantiation or explicit specialization (...))

Это можно использовать следующим способом:
Допустим у нас есть класс

#include <iostream>

class Foo { int field; void print() { std::cout << field << '\n'; } };

Мы хотим положить указатели на члены класса field и print в следующие глобальные переменные.

inline int Foo::* Foo_field_ptr;
inline void (Foo::* Foo_print_ptr)();

Напишем шаблонный класс, который принимает указатель на глобальную переменную, и какое-то значение.
В инициализаторе поля _ мы будем присваивать значение переменной.

template<auto* Ptr, auto Val>
class SetVar {
    static const bool _;
};

template<auto* Ptr, auto Val> inline const bool SetVar<Ptr, Val>::_ = (*Ptr = Val, false); // оператор "запятая" ^

Теперь мы можем инстанциировать SetVar указателем на переменную Foo_field_ptr и указателем на член класса&Foo::field, сработает инициализатор SetVar::_, и значение запишется в переменную.

template class SetVar<&Foo_field_ptr, &Foo::field>;
template class SetVar<&Foo_print_ptr, &Foo::print>;

int main() { Foo x; x.Foo_field_ptr = 42; (x.Foo_print_ptr)(); }


Альтернативная версия с использованием функции-друга

// Привязывает член класса к некоторому типу-тегу,
// и определяет функцию, которая возвращает указатель на член класса.
template<typename Tag, auto MemPtr>
class PrivateMemberAccess {
    friend constexpr auto GetPrivateMemPtr(Tag) {
        return MemPtr;
    }
};

// Всмомогательный тип чтобы писать <T> вместо (T{}) template<typename Tag> static constexpr auto PrivateMemberPtr = GetPrivateMemPtr(Tag{});

Класс-жертва

#include <iostream>

class Foo { int field; void print() { std::cout << field << '\n'; } };

Бойлерплейт для доступа к приватным членам класса

struct Foo_field_tag {};
struct Foo_print_tag {};

template class PrivateMemberAccess<Foo_field_tag, &Foo::field>; template class PrivateMemberAccess<Foo_print_tag, &Foo::print>;

// Если не объявить функцию, то мы не сможем её вызвать. constexpr auto GetPrivateMemPtr(Foo_field_tag); constexpr auto GetPrivateMemPtr(Foo_print_tag);

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

int main() {
    Foo x;
    x.*PrivateMemberPtr<Foo_field_tag> = 42;
    (x.*PrivateMemberPtr<Foo_print_tag>)();
}
Abyx
  • 31,143
  • Огого! Стандарты C++ темны и непонятны. Ужас. А как вы оцениваете более простой идеологически метод Qwertiy? – VladD Oct 03 '15 at 14:37
  • Это-то я понимаю. Вроде бы нарушения нету, т. к. фиктивный класс используется лишь для получения offsetof, а разыменовывается только double*. Вопрос в том, имеем ли мы право разыменонвывать double*, который указывает на поле? То есть, совместимость double Class::* с double*. – VladD Oct 03 '15 at 15:47
  • Кслассный способ :) – Qwertiy Oct 03 '15 at 16:23
  • @VladD, а почему бы нам не иметь такого права? Если бы он был public, то мы могли бы передавать указатель на него в функцию, принимающую указатель на double. Это по сути и есть double - это же не метод, где часть типа с классом прячет передачу this, это просто значение. – Qwertiy Oct 03 '15 at 16:25
  • @Qwertiy: Хм, убедили. – VladD Oct 03 '15 at 16:40
  • @VladD, я похоже немного ошибся про указатель на double как член класса - судя по коду это то самое offsetof - иначе было бы невозможно скормить шаблону инстанс объекта. Но на правильность рассуждений это не влияет. – Qwertiy Oct 03 '15 at 16:47
  • 1
    А что-то более приемлемое, без глобальной переменной, возможно? Но, в любом случае, великолепный пример, почему не нужно использовать C++. – avp Oct 03 '15 at 16:51
  • @Abyx: Кажется, тогда можно убрать фиктивную переменную static bool _ и использовать вместо неё mem_ptr? – VladD Oct 03 '15 at 18:28
  • @Abyx: Я имел в виду, вместо bool set_member<Getter, P>::_ = (Getter::mem_ptr = P, false); писать Getter::mem_ptr set_member<Getter, P, ...>::_ = P;, как-то так. (Если такое пройдёт.) – VladD Oct 03 '15 at 18:36
  • @Abyx: Ой-ой-ой, теперь только понял! set_member существует и оказывает сайд-эффекты, но к нему невозможно получить доступ. Мм-да, хорошая лазейка в законе. – VladD Oct 03 '15 at 19:17
  • И все-таки я абсолютно не могу понять эту "шаблонную магию". Совершенно не доходит (даже с Helper, т.е. в ответе) как именно (по шагам) они работают, когда выполняется i = obj.*g_mem_ptr; / (а интересно, ТС, ради которого по идее и сделан ответ, всю эту механику понял?) – avp Oct 03 '15 at 22:45
  • Поигрался немного с отладчиком и кодом. Кстати, добиться доступа к нескольким полям (добавил int j; в Cls) с помощью "библиотечной" версии не удалось (а вот с "разноименными" копиями Helper -- все получается) / Однако, согласитесь, многовато букв требуется для того, что обеспечивает простая копия Cls со всеми public, которую просто накладываем на оригинал) – avp Oct 04 '15 at 14:40
  • Ну, не знаю. Если делаем полную копию со всеми методами, но вместо private всюду public, то вроде все правильно печатает. – avp Oct 04 '15 at 19:12
  • В теории может, но на практике вряд ли кто-то станет этим заниматься в зависимости от public/private – avp Oct 04 '15 at 22:16
3

http://ideone.com/gTyt2Q

#include <stdio.h>
#include <stddef.h>

struct cls
{
  public:
    cls(char c, double d, int i) : c(c), d(d), i(i) {}
  private:
    char c;
    double d;
    int i;
};

struct copy
{
  public:
    copy(char c, double d, int i);
  public:
    char c;
    double d;
    int i;
};

int main(void)
{
  cls x('1', 2.5, 3);

  printf
  (
    "%c %f %d\n", 
    *(char*)((void*)&x + offsetof(copy, c)),
    *(double*)((void*)&x + offsetof(copy, d)),
    *(int*)((void*)&x + offsetof(copy, i))
  );

  return 0;
}

Вроде бы в комментариях пришли к выводу, что этот код не содержит UB, поскольку:

  • Структуры с одинаковым набором и последовательностью полей имеют одинаковое внутреннее представление.
  • Приведение указателя к другому типу не является UB до тех пор, пока не делается его разыменование. Кроме того, в данном коде приведение делается только к void*.
  • Добавление смещения к адресу структуры даёт указатель на её поле.
  • Указатель на поле приводится к корректному типу, поэтому его разыменование не приводит к UB.
Qwertiy
  • 123,725
0

Уже, наверное, неактуально, но всё же оставлю здесь свой вариант)

#include <iostream>
using namespace std;
class lol
{
    int x;
public:
    lol()
    {
        x = 5;
    }
    void pr()
    {
        cout << x << endl;
    }
};

int main() { lol big; big.pr(); int* x = reinterpret_cast<int>(&big); cout << x << endl; return 0; }

пробежался по ответам, reinterpret_cast нигде не увидел

HMS_TRINIDAD
  • 118
  • 7
0

А можно и без функций для доступа (и для разнообразия, а то что-то все про union забыли).

int main (int ac, char *av[]) {
  union u {
    struct Cls hidden;
    struct B   B;
  } *up;


  Cls cls('h', 2.0, 3);
  B b('A', 3.14, 10);
  up = (union u *)&cls;

  cout << up->B.c1 << ' ' << up->B.d1 << ' ' << up->B.i1 << '\n';
  up->B = b;
  cout << up->B.c1 << ' ' << up->B.d1 << ' ' << up->B.i1 << '\n';

}

Результат

avp@avp-ubu1:hashcode$ g++ c.cpp -std=c++11
avp@avp-ubu1:hashcode$ ./a.out 
h 2 3
A 3.14 10
avp@avp-ubu1:hashcode$ 

Если же использовать -std=gnu++11, то union можно сделать безымянным, а приведение типа up = (typeof(up))&cls; вот так.

avp
  • 46,098
  • 6
  • 48
  • 116
  • 2
    Так всё равно ж UB. Из union по стандарту можно читать вроде только то, что туда фактически положено. (Трюки для превращения double в байты не соответствуют стандарту.) – VladD Oct 01 '15 at 17:15
  • @VladD, ну ведь на практике это работает. А где не заработает, там надо либо сменить компилятор, либо ... придумать что-нибудь еще (по обстановке). Все равно ведь абсолютная переносимость программ это нечто идеальное (вроде коммунизма) – avp Oct 01 '15 at 17:20
  • 1
    Ну, это на gcc. Плюс если там реально UB, то в любой момент (то есть в любой новой версии компилятора) оптимизатор может броситься на амбразуру грудью и снести всю функцию. – VladD Oct 01 '15 at 17:22
  • @VladD, в таком случае придется вспомнить ассемблер -) – avp Oct 01 '15 at 17:27
0

Вам нужно применить паттерн "Паблик Морозов"

#define private public
#define protected public
#include "your class header.h"
#undef private
#undef protected

После такого подключения все закрытые поля класса, объявленного в Yourclass header.h станут пабликами.

0
class A
{
#ifdef BOOST_TEST_MODULE
public:
#endif
Serg Kryvonos
  • 222
  • 1
  • 7