Как стать автором
Обновить

Комментарии 30

Красиво, думаю это в скором времени появится в тестовых фреймворках

Да, думаю, что это станет отличным инструментом для тестовых фреймворков и других библиотек, которые полагаются на доступ к внутреннему устройству классов. Читал о рефлексии, но чувствовалось, что её возможности ограничены — особенно в плане доступа к приватным методам. Скорее всего, такие изменения сделают рефлексию гораздо более полноценной и гибкой.

Если в наличии есть C++20, то подход из статьи https://dfrib.github.io/a-foliage-of-folly/ мне кажется проще и выразительнее.
Кстати никакой из встреченных мной способов не умеет работать с битовыми полями (потому что нельзя взять адрес на такое поле), так что параноики могут использовать это для защиты от таких трюков (но вам не защититься от тех, кто эксплуатирует ODR мухахахаха)!

там нет никаких отличий в статье, просто там не готовое решение, а показан принцип. И используется auto вместо типа напрямую, это незначительное изменение

Да, видимо проморгал.
Готовое решение тоже указано в статье: https://github.com/dfrib/accessprivate

Спасибо за ссылку — интересный подход с использованием C++20!
Я пока придерживаюсь C++17 по ряду проектных причин и личных предпочтений. На мой взгляд, переход на C++20 пока не даёт таких критических преимуществ, чтобы тянуть за собой всю инфраструктуру.
К тому же, в некоторых случаях старые добрые методы не только работают, но и проще читаются теми, кто ещё не "на ты" с новыми фишками стандарта.

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

Принцип довольно давно известный, мне кажется был в одной из книжек Саттера. Я сейчас использую домашнее решение компании с gtest, которое точно так же через специализацию шаблона предоставляет доступ к приватам. В тестируемом классе объявлен friend template class, а тест его специализирует и спокойно обращается к приватам без указателей на поля и прочих усложнений.

В тестируемом классе объявлен friend template class

это совершенно другое. Требует модификации самого тестируемого типа. С таким же успехом можно заменить private на public

Разобрался. Сбили с толку вот эти слова из статьи:

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

Думал, что все эти макросы вставляются в сам класс. Без этого и правда интересно и сильно отличается от привычной интеграции через friend.

Зачем нужен забор?
Чтобы ограничить проход для тех, у кого нет ключа от калитки.
Что сделать на случай если забудешь ключ от калитки?
Нужно сделать дырку в заборе.

Почему нельзя просто взять указатель на приватное поле?

Нельзя, потому что НЕЛЬЗЯ. Его поэтому и сделали приватным.

Компилятор запретит это, потому что name — приватное поле.

Компилятор запрещает, потому что НЕЛЬЗЯ. Это защита от погроммистов.

Если предложенный метод доступа является безопасным, то следует обосновать его безопасность. Иначе это способ обмануть компилятор. Теоретически, можно сделать чтобы компилятор выявлял такие вещи.

Вдохновлённый статьёй «Когда private, но очень хочется public», я решил исследовать альтернативный способ доступа к приватным членам класса в C++

Но в том случае прикручивали тестирование к чужому проекту. Поэтому были вынуждены нарушать правила.

Спасибо за интерес к статье

«Нельзя, потому что нельзя»

Это не объяснение, а тавтология. Да, программировании ограничения существуют не просто так: они служат для поддержания инвариантов, предотвращения несанкционированного доступа, обеспечения безопасности и читаемости кода. Однако бывают ситуации, когда эти ограничения необходимо преодолеть — например, при тестировании legacy-кода без возможности изменения интерфейса, работе с закрытыми API (например, встраивание функционала в чужой движок), исследовательских целях или reverse-engineering'е

То, что технически запрещено, не всегда является запретом на использование в специфических условиях. Это как сказать, что у автомобиля есть замок зажигания, значит его вообще нельзя завести без ключа. Но в сервисном центре или при диагностике иногда это нужно — и для этого тоже есть способы, только делают это опытные люди с пониманием последствий.

«Компилятор запрещает, потому что нельзя»

Это неверно с точки зрения реализации и стандартов языка. Компилятор запрещает из-за правила [class.access] стандарта C++, которое регламентирует видимость членов класса. Это не произвол — это формальная часть языка, обеспечивающая защиту от случайного или намеренного нарушения инкапсуляции. В реальности вы просто повторяете то, что уже указано в статье.

Компилятор запретит это, потому что name — приватное поле. Это описано в стандарте C++ в разделе [class.access] , где указано, что доступ к приватным членам класса (private) разрешён только для методов самого класса и дружественных (friend) сущностей. Любая попытка обращения к private-полю извне класса приводит к ошибке компиляции, так как нарушает правила инкапсуляции.

«Если метод безопасен, нужно это обосновать»

Метод, описанный в статье, использует шаблоны и статические инициализаторы, которые работают на этапе компиляции и не нарушают абстракцию памяти или ABI. Он не использует const_cast, reinterpret_cast или другие потенциально опасные конструкции. Он не обходит защиту памяти или runtime-проверки — он просто использует легальные механизмы языка, позволяющие получить доступ к приватным членам через явную специализацию шаблонов, которая происходит в допустимом контексте.

Это не "дырка в заборе", это скорее "ключ к резервному входу". Использование такого способа требует знания того, что вы делаете.

«Можно сделать, чтобы компилятор это находил»

Да, можно. Но тогда вы усложните язык, сделаете невозможными многие легитимные вещи вроде SFINAE, частичной специализации, трейтов, сериализации и других продвинутых возможностей. Язык C++ построен на принципе: вы имеете право сломать себе ногу, если вы сами взяли молоток и гвозди.

Такие техники, как описанная в статье, не являются злоупотреблением — они расширяют границы возможного в условиях ограниченного доступа к исходникам, устаревшего кода или необходимости глубокого анализа объектов.

«Тестирование чужого проекта»

Да, вы правы: я не тестировал чужой legacy-код, который сломается, если я не достану изнутри private-поле. Я не сидел в уголке, зажав в руках документацию от библиотеки 1993 года, и меня никто не заставлял это делать.

Но знаете что? Это не делает мой подход бесполезным.

Если бы все писали статьи только про то, что сильно болит , мы бы до сих пор писали на ассемблере — потому что "ну вот так нам нужно". Иногда исследование возможностей ради интереса приводит к чему-то полезному. Иногда оно рождает инструмент, который через пару лет спасёт чью-то пятую точку. А иногда просто демонстрирует, что язык ещё не догнал своими ограничениями возможности разработчиков.

Моя задача была не в том, чтобы решить конкретную проблему здесь и сейчас, а показать технически корректный способ , как можно обойти ограничения доступа без нарушения семантики языка. Ни один reinterpret_cast не пострадал. Не было #define-хаков, макросных трюков или указателей void*. Всё работает строго на этапе компиляции, используя механизмы, которые и так есть в C++: шаблоны, специализации, статическую инициализацию.

Это паттерн использования языка , который позволяет вам получить доступ к данным, не нарушая при этом ABI, memory model и логику типобезопасности. Такие вещи важны, когда вы пишете библиотеки, фреймворки или инструменты, которые должны быть надёжными даже в условиях ограниченного контроля над исходным кодом.

Если вам кажется, что я просто «развил идею», то вы, возможно, недооцениваете разницу между тем, чтобы пробить дырку в заборе , и тем, чтобы найти запасной ключ , который всегда был, но никто не проверял.

Инженерия — это не всегда решение проблемы. Иногда это подготовка решения к проблеме, которую вы ещё не видели.

Спасибо за подробный ответ. По стилю комментарий отменный. Такое развернутое вступление украсило бы статью.

Вот два вопроса по теме.
Почему нет стандартного решения для доступа к приватным элементам на этапе отладки?
Как доказать что предложенный метод не несет потенциальных угроз?

Сипипи не вчера изобрели. И делали его неглупые люди. Стандарты разработаны для безопасности и удобства. Неужели в него не встроены стандартные методы предоставления доступа в приватам на этапе отладки?
Если ваш метод полезен, значит сипипи по своей природе ущербен, и требует таких вот костылей, пусть и весьма изящных.

То, что технически запрещено, не всегда является запретом на использование в специфических условиях.

Можно было усилить акцент на "специфических условиях". То есть " в условиях ограниченного доступа к исходникам, устаревшего кода или необходимости глубокого анализа объектов." Чтобы новички не принимали это за норму.

Это не делает мой подход бесполезным.

Речь не об этом. Любое нестандартное решение несет потенциальную угрозу. Не приведет ли предложенный метод к возникновению дыры в безопасности?

Метод, описанный в статье, использует шаблоны и статические инициализаторы, которые работают на этапе компиляции и не нарушают абстракцию памяти или ABI. Он не использует const_cast, reinterpret_cast или другие потенциально опасные конструкции.

Этот текст тоже должен быть в статье.
Звучит красиво. Но это больше декларация, чем доказательство.

Если вам кажется, что я просто «развил идею», то вы, возможно, недооцениваете разницу между тем, чтобы пробить дырку в заборе , и тем, чтобы найти запасной ключ , который всегда был, но никто не проверял.

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

Почему нет стандартного решения для доступа к приватным элементам на этапе отладки?

Потому что C++ — это язык, в котором инкапсуляция — не просто ограничение доступа, а один из фундаментальных принципов проектирования. Стандарт не предусматривает механизмов для нарушения приватности даже с учётом нужд отладки, ведь добавление такого функционала противоречило бы самой философии языка. Программа должна быть строгой и защищённой по своей природе, а если разработчику нужно «заглянуть внутрь» объекта — это уже его личная ответственность и дело техники, а не задача языка.

Как доказать что предложенный метод не несет потенциальных угроз?

  1. Метод использует легальные средства языка C++ , включая шаблоны, указатели на члены класса, статическую инициализацию и явную специализацию. Все эти механизмы описаны в стандарте C++ и не предполагают undefined behavior при корректном использовании.

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

  3. Все операции происходят на этапе компиляции , что исключает runtime-обход защиты или манипуляции памятью. Это позволяет компилятору полностью контролировать типобезопасность и layout объектов.

Сипипи не вчера изобрели. И делали его неглупые люди. Стандарты разработаны для безопасности и удобства. Неужели в него не встроены стандартные методы предоставления доступа в приватам на этапе отладки?Если ваш метод полезен, значит сипипи по своей природе ущербен, и требует таких вот костылей, пусть и весьма изящных.

Отладка — это внешний процесс, и она не должна влиять на дизайн API. Язык не предоставляет стандартных способов доступа к приватным членам, потому что инкапсуляция — ключевой принцип ООП. Любые обходные пути возникают не из-за недостатков С++, а как побочный эффект его гибкости и низкоуровневой природы. Сказал бы, что не баг, а особенность языка: даёт контроль, но не навязывает ограничения через силу.

Речь не об этом. Любое нестандартное решение несет потенциальную угрозу. Не приведет ли предложенный метод к возникновению дыры в безопасности?

Cледует различать два принципиальных аспекта: неопределённое поведение (undefined behaviour, UB) и доступ к данным.

С точки зрения UB, использование указателей на члены класса для получения доступа к приватным полям не приводит к неопределённому поведению при соблюдении всех правил работы с такими указателями. Если компилятор знает типы и они корректно используются, то такой способ остаётся в рамках стандарта C++. То есть формально никакого нарушения стабильности или предсказуемости программы не происходит — всё выполняется строго по правилам языка.

Можно провести параллель с отладчиком. Допустим, мы создаём объект класса Dog, передавая ему имя через конструктор, но ни одно поле или метод не являются публичными. Но любой отладчик покажет содержимое внутренних полей, потому что имеет информацию о структуре класса и может прочитать память. Описываемый подход делает то же самое, только программно. Он позволяет получить доступ к внутреннему состоянию объекта даже тогда, когда публичный интерфейс не предоставляет такой возможности. Это не обход защиты, а скорее расширение возможностей разработчика в сложных ситуациях.

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

Этот метод не создаёт реальных дыр в безопасности, которые можно было бы использовать злоумышленнику. Он работает исключительно внутри вашей программы, при наличии полного контроля над кодом и сборкой. Это не обход ограничений, а возможность, аналогичная возможностям отладчика. При грамотном использовании она не представляет опасности. Если же такой приём используется неправильно — это уже вопрос квалификации и ответственности разработчика, а не самого механизма.

Но это больше декларация, чем доказательство.

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

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

Да, это действительно запасной ключ потому что метод опирается на существующие языковые конструкции — шаблоны и статические инициализаторы — которые работают на этапе компиляции, не нарушая абстракции памяти или ABI, и не требуют изменения исходного кода класса.

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

А я не понял как это работает. Если специализация идет не изнутри класса, то разве взятие адреса приватной переменной доступно? Адрес берется для инстанцирования шаблона, но в начале статьи ведь упоминалось, что нельзя взять адрес приватной переменной.

template struct init_member<Dog, string, &Dog::name>;

Шаблоны в C++ инстанцируются на этапе компиляции, и если специализация шаблона происходит в контексте, где приватный член доступен (например, внутри класса или через friend), то компилятор разрешит взять его адрес

В коде я не вижу ни "внутри класса" ни "friend".

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

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

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

Более того, некоторые "расширения", вроде RTTI, уже показывают, что такое разделение возможно: стандарт определяет интерфейс, а реализация остаётся на усмотрение компилятора. В будущих версиях языка ожидается полноценная поддержка рефлексии, которая должна быть добавлена именно таким образом — без нарушения ключевых принципов C++.

Я новичок в C++, и думаю, что словлю массу хейта. А старый добрый friend и всякие разновидности его использования уже "не канают"? Тупой вопрос, я понимаю, но может гуру разжуют и пинать не будут?

friend — вполне нормальный и рабочий способ дать доступ к приватным членам другим функциям или классам. Он есть в C++ с самого начала и работает хорошо. Данный "велосипед" необходим, если нельзя изменить исходный код класса — например, он из библиотеки. Или если вам нужно получить доступ ко всем приватным полям и методам, а не только к одному конкретному. Ещё бывает удобно сделать это автоматически, через шаблоны, без ручного указания каждого члена. Вот тогда и придумывают разные хитрые способы, подобно этому но при этом сохранить типобезопасность и работать на этапе компиляции.

Использование #define private public может вызвать побочные эффекты, поскольку препроцессор меняет не только нужное вам слово в конкретном классе, но и во всех заголовочных файлах, которые вы подключаете после этой директивы — в том числе из стандартной библиотеки или сторонних библиотек.

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

Спасибо 😊

Мне больше нравится такой способ:

class Fruit
{
public:

    void name  () const { cout << m_Name  ; }
    void weight() const { cout << m_Weight; }
    void price () const { cout << m_Price ; }

private:

    string m_Name   = "Apple";
    float  m_Weight = 212.52;
    int    m_Price  = 500;
};

struct FruitAccessor
{
    string name;
    float  weight;
    int    price;
    // При желании, поля можно сделать приватными, а доступ к ним организовать сеттерами.
};
    Fruit f;
    f.name  (); // Apple
    f.price (); // 212.52
    f.weight(); // 500

    FruitAccessor *accessor;
    accessor = reinterpret_cast<FruitAccessor*>(&f);
    accessor->name   = "Banana";
    accessor->price  = 2000;
    accessor->weight = 634.64;
    
    f.name  (); // Banana
    f.price (); // 2000
    f.weight(); // 634.64

Думаю, тут полно простора для доработки и улучшений, я лишь предоставил базовую идею.

Тоже вариант. Вот пример доработки вашего метода

#include <iostream>
#include <string>
using namespace std;

// ==================== Базовый класс Fruit ====================
class Fruit {
public:
    Fruit() = default;
    void name()   const { cout << m_Name << endl; }
    void weight() const { cout << m_Weight << endl; }
    void price()  const { cout << m_Price << endl; }

private:
    string m_Name = "Apple";
    float  m_Weight = 212.52;
    int    m_Price = 500;
};

// Объявляем структуру с тем же layout, что и private-поля в Fruit
struct FruitAccessor
{
    string m_Name;
    float  m_Weight;
    int    m_Price;
};

int main() {
    Fruit f;

    // Приводим указатель на Fruit к указателю на FruitAccessor
    FruitAccessor* accessor = reinterpret_cast<FruitAccessor*>(&f);

    // Получаем ссылку на m_Name
    string& nameRef = accessor->m_Name;

    cout << "Before change: ";
    f.name(); // Apple

    nameRef = "Banana";

    cout << "After change: ";
    f.name(); // Banana
}

Проблема данного подхода в том, что он опирается на конкретное расположение полей в классе Fruit. Если изменить порядок следования полей в классе или добавить/удалить какие-либо из них, структура FruitAccessor перестанет соответствовать layout-у класса Fruit, и доступ к памяти через reinterpret_cast станет некорректным. Это приведёт к неопределённому поведению (undefined behaviour)

Жаль только, что UB.

Проблема не только в неопределённом поведении (UB) , но и в том, что для доступа к одному приватному полю или методу приходится создавать полностью совпадающую "зеркальную" структуру , повторяющую весь layout исходного класса.

Например, если тебе нужно получить доступ всего к одному полю, находящемуся на 100-й позиции в списке полей класса, всё равно приходиться вручную перечислить предыдущие 99 полей в правильном порядке и с тем же типом. Это не просто неэффективно — это практически нереализуемо в реальных проектах.

Это, допустим, макросами решается, пусть даже и будет выглядеть всрато.

А вот такое оно сможет переварить?

class Foo{
  private:
    int a;
    int b;
    std::string& c;
};

Вообще я сам игрался с указателями на поля члены, делал на этом сериализацию. Для меня стали проблемой обращение к полям ссылкам и базовому классу.
Код есть тут (с++17):
https://github.com/aethernetio/aether-client-cpp/blob/main/aether/reflect/reflect.h
Много пришлось подмазать макросами. Ну и в моем случае необходимость менять класс это фича, а не проблема.

А если хотите чтоб не знали, то документацию на приввтные методы не пишите. Все ломается, но как….

Подскажите пожалуйста как работает первый пример из вашей статьи в случае если класс Dog имеет несколько приватных полей типа std::string и нужно организовать доступ ко всем таким полям?

не работает

Поясните, плиз, для новичка в плюсах.

Почему вот тут

template class PrivateMemberAccessor<Dog, std::string, &Dog::name>;

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации