Частные члены структуры в C с const

Чтобы иметь чистый код, может быть полезно использовать некоторые концепции объектно-ориентированного программирования, даже в C. Я часто пишу модули, состоящие из пары файлов .h и .c. Проблема в том, что пользователь модуля должен быть осторожен, так как закрытые члены не существуют в C. Использование идиомы pimpl или абстрактных типов данных допустимо, но это добавляет некоторый код и/или файлы и требует более тяжелый код. Я ненавижу использовать аксессуар, когда он мне не нужен.

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

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

/*** 2DPoint.h module interface ***/
#ifndef H_2D_POINT
#define H_2D_POINT

/* 2D_POINT_IMPL need to be defined in implementation files before #include */
#ifdef 2D_POINT_IMPL
#define _cst_
#else
#define _cst_ const
#endif

typedef struct 2DPoint
{
    /* public members: read and write for user */
    int x;
    
    /* private members: read only for user */
    _cst_ int y;
} 2DPoint;

2DPoint *new_2dPoint(void);
void delete_2dPoint(2DPoint **pt);
void set_y(2DPoint *pt, int newVal);


/*** 2dPoint.c module implementation ***/
#define 2D_POINT_IMPL
#include "2dPoint.h"
#include <stdlib.h>
#include <string.h>

2DPoint *new_2dPoint(void)
{
    2DPoint *pt = malloc(sizeof(2DPoint));
    pt->x = 42;
    pt->y = 666;

    return pt;
}

void delete_2dPoint(2DPoint **pt)
{
    free(*pt);
    *pt = NULL;
}

void set_y(2DPoint *pt, int newVal)
{
    pt->y = newVal;
}

#endif /* H_2D_POINT */


/*** main.c user's file ***/
#include "2dPoint.h"
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    2DPoint *pt = new_2dPoint();

    pt->x = 10;     /* ok */
    pt->y = 20;     /* Invalid access, y is "private" */    
    set_y(pt, 30);  /* accessor needed */
    printf("pt.x = %d, pt.y = %d\n", pt->x, pt->y);  /* no accessor needed for reading "private" members */

    delete_2dPoint(&pt);
    
    return EXIT_SUCCESS;
}

А теперь вопрос: подходит ли этот трюк для стандарта C? Он отлично работает с GCC, и компилятор ни на что не жалуется, даже с некоторыми строгими флагами, но как я могу быть уверен, что это действительно нормально?


person MouleMan    schedule 26.12.2012    source источник
comment
Интересный подход. Является ли это четко определенным поведением, я не знаю. Я бы рекомендовал против этого, потому что это далеко от идиоматичного C... либо используйте непрозрачную структуру (определенную в файле .c) и предоставляйте методы доступа, либо документируйте, которым поля не должны назначаться.   -  person Thomas    schedule 26.12.2012
comment
Я думаю, что ответ Томаса должен быть настоящим ответом - возможно, с парой примеров.   -  person Mats Petersson    schedule 26.12.2012
comment
Кстати, как 2DPoint формирует действительный идентификатор?   -  person    schedule 26.12.2012
comment
Я подробно остановился на ответе Томаса (случайно не увидел комментарий) в ответе H2CO3, если вы хотите лучше понять, как работают непрозрачные структуры.   -  person Jonathan Grynspan    schedule 26.12.2012


Ответы (4)


Это нарушает C 2011 6.2.7 1.

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

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

Это крайне неправильное программирование.

person Eric Postpischil    schedule 26.12.2012
comment
@ H2CO3, не могли бы вы объяснить немного лучше. Вопреки вашему ответу, этот здесь дает правильный ответ, включая ссылку на стандарт, который показывает, почему это UB. - person Jens Gustedt; 27.12.2012
comment
@JensGustedt Посмотрите на правку. Первоначально Эрик предоставил ответ относительно C++. С тех пор он удалил свои комментарии и отредактировал свой ответ. Я также удалил свой уже устаревший комментарий. - person ; 27.12.2012
comment
@ H2CO3, даже если раньше это был ответ для C ++, идеи по этому поводу кажутся похожими на двух языках, только формулировка отличается. В любом случае, теперь в этом ответе здесь используется правильная формулировка, и это дает хорошую мотивацию не только того, почему это UB, но и того, почему эти два разных типа могут действительно иметь разную компоновку. - person Jens Gustedt; 27.12.2012
comment
@JensGustedt Да, именно поэтому я удалил свой комментарий. Раньше на формулировку жаловались именно вы, так что не будем продолжать эту бессмысленную дискуссию. Я был прав, Эрик тоже прав, ты больше никого не дразнишь, и будет мир. Пока. - person ; 27.12.2012

Это почти наверняка неопределенное поведение.

Запись/изменение объекта, объявленного как const, запрещена и приводит к UB. Кроме того, выбранный вами подход повторно объявляет struct 2DPoint как два технически разных типа, что также недопустимо.

Обратите внимание, что это (как неопределенное поведение в целом) не означает, что оно «точно не будет работать» или «должно произойти сбой». На самом деле, я нахожу вполне логичным, что это работает, потому что если кто-то внимательно читает источник, он может легко выяснить, какова его цель и почему он может считаться правильным. Однако компилятор не является разумным — в лучшем случае это конечный автомат, который ничего не знает о том, что код должен делать; он только подчиняется (более или менее) синтаксическим и семантическим правилам грамматики.

person Community    schedule 26.12.2012
comment
Это конечно неопределенное поведение (не почти) ;) - person netcoder; 26.12.2012
comment
Apple любит непрозрачные шрифты для этой цели. У них будет typedef struct Foo *FooRef; в заголовке, функции, которые принимают или возвращают FooRef, а затем определение struct Foo в реализации перед этими функциями. По сути, в структуре нет открытых членов, и все делается с помощью этих функций (FooRef FooCreateWithWidget(WidgetRef w), GrommetRef FooGetGrommet(FooRef foo) и т. д.). - person Jonathan Grynspan; 26.12.2012
comment
@JonathanGrynspan Но тогда структуры не преобразуются в const, так что это не UB. - person ; 26.12.2012
comment
@ H2CO3: Нет, я знаю - я говорил, что это альтернативный подход к этой общей проблеме C, который позволяет избежать проблемы константности. Однако ответ на ваш ответ, возможно, был неправильным местом, чтобы упомянуть об этом. - person Jonathan Grynspan; 26.12.2012
comment
@JonathanGrynspan Ах, хорошо, извините за недоразумение. Да все верно. - person ; 26.12.2012
comment
@EricPostpischil Мы говорим о C. - person ; 26.12.2012
comment
Тот же вопрос для C. Рассматриваемый код не изменяет идентификатор, объявленный как const. Он изменяет объект, объявленный без константы, который был неправильно связан с типом, в котором объект является константой. - person Eric Postpischil; 26.12.2012
comment
@EricPostpischil Да, это правильно, но с точки зрения другого файла этот член все еще является константой. - person ; 26.12.2012
comment
Какого АДА ты минусуешь это? - person ; 26.12.2012
comment
@ H2CO3, это был не я, но тоже соблазнился. Ваш ответ неточен на грани лжи. Идентификаторы в C определенно не имеют типа или могут быть const-квалифицированными. Объекты имеют тип, который может быть квалифицирован const, и доступ к такому объекту типа const является UB. Так что это правило само по себе не поможет в вопросе UB. Эрик дает правильный ответ. - person Jens Gustedt; 27.12.2012
comment
@JensGustedt Я обновил ответ, чтобы использовать правильную терминологию. Но: так как объект имеет спецификацию const и к нему обращаются, это то, что вызывает UB. - person ; 27.12.2012
comment
@ H2CO3 неправильно. Тип объекта определяется в единице компиляции, где нет const, поэтому тип объекта не является const-квалифицированным. Таким образом, можно передать такой объект обратно в эту единицу компиляции, чтобы изменить его. Повторюсь, ваше представление о том, почему это будет UB, было неверным, Эрик дал правильный ответ, и теперь ваш ответ копирует его, не ссылаясь на Эрика. - person Jens Gustedt; 27.12.2012
comment
@JensGustedt О чем ты говоришь? Вы обвиняете меня в плагиате? За изменение ОДНОГО слова ради терминологии, на которую вы жаловались? Оставь меня в покое сейчас... - person ; 27.12.2012
comment
@ H2CO3: в стандарте C нет такого понятия, как объект с указанием const. Существуют константные типы. Существует правило против доступа к объекту, определенному с константным типом, с lvalue неконстантного типа, но это не будет применяться здесь, поскольку объект будет определен с неконстантным типом, а константный тип будет использоваться только для доступа. Укажите точное правило в C, которое нарушено. - person Eric Postpischil; 27.12.2012
comment
@EricPostpischil Может быть, это неясно, но я говорю то же самое, что вы сказали в своем ответе. Я специально имею в виду, что два определения одной и той же структуры в разных единицах перевода имеют часть совместимого типа. - person ; 27.12.2012
comment
[Исправленная опечатка.] @H2CO3: Вы отказываетесь от своего заявления о том, что «поскольку объект имеет спецификацию const и он [модифицирован], именно это вызывает UB»? Поскольку объект не является константным (поскольку такой вещи не существует), и объект не определен константным типом, поэтому его изменение само по себе не является неопределенным поведением. Если вы не отзываете его, укажите конкретный пункт стандарта C, который нарушен. - person Eric Postpischil; 27.12.2012

По словам Бьерна Страуструпа: C не предназначен для поддержки ООП, хотя он позволяет ООП, что означает, что на C можно писать ООП-программы, но это очень сложно так. Таким образом, если вам нужно писать ООП-код на C, кажется, нет ничего плохого в использовании этого подхода, но предпочтительнее использовать язык, лучше подходящий для этой цели.

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

Изменить Возможно, вам придется использовать приведение, чтобы обойти const. Я не могу вспомнить, можно ли использовать приведение в стиле C, как C++ const_cast.

person Masked Man    schedule 26.12.2012
comment
Запись в объект, константность которого была отброшена, в любом случае является UB. - person netcoder; 26.12.2012
comment
Ага, понятно. Кажется, я припоминаю, что читал, что константность можно безопасно отбросить, но, возможно, я ошибаюсь. Я посмотрю это снова. Спасибо. - person Masked Man; 26.12.2012
comment
Ах, я вижу проблему. Я думал, что вопрос был о том, можно ли использовать этот подход. Я пропустил часть о том, безопасно ли это с другими компиляторами, кроме gcc. Извини за это. - person Masked Man; 26.12.2012

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

Вы всегда передаете указатель на struct и при необходимости приводите его к внутреннему использованию, например:

/* user code */
struct foo {
    int public;
};

int bar(void) {
    struct foo *foo = new_foo();
    foo->public = 10;
}

/* implementation */
struct foo_internal {
    int public;
    int private;
};

struct foo *new_foo(void) {
    struct foo_internal *foo == malloc(sizeof(*foo));
    foo->public = 1;
    foo->private = 2;
    return (struct foo*)foo;  // to suppress warning
}

C11 допускает неименованные поля структуры (GCC некоторое время поддерживает это), поэтому в В случае использования GCC (или компилятора, совместимого с C11) вы можете объявить внутреннюю структуру как:

struct foo_internal {
    struct foo;
    int private;
};

поэтому не требуется никаких дополнительных усилий для синхронизации определений структуры.

person qrdl    schedule 26.12.2012
comment
Это не реализует функцию, согласно которой «не друзья» класса могут читать, но не записывать членов. - person Eric Postpischil; 27.12.2012
comment
@EricPostpischil Согласен, но вы можете использовать для этого сеттеры/геттеры. Я думаю, что это самое близкое, что можно получить, поэтому нет причин минусовать - person qrdl; 27.12.2012
comment
Вопрос указывает на то, что следует избегать средств доступа: «Я ненавижу использовать средства доступа, когда они мне не нужны». Этот ответ не решает поставленную проблему. - person Eric Postpischil; 27.12.2012
comment
@EricPostpischil Мой ответ действительно не решает проблему, но эта проблема не может быть решена, поэтому я предложил наиболее близкое возможное решение. - person qrdl; 27.12.2012