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

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

Ето една идея, която предоставя начин да накарате компилатора да се оплаква от невалиден достъп до частни членове, само с няколко допълнителни кода. Идеята е да се дефинира два пъти една и съща структура, но с добавен допълнителен 'const' за потребителя на модула.

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

/*** 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: Не, знам - казвах, че това е алтернативен подход към този често срещан проблем със С, който избягва проблема с постоянството. Отговорът на вашия отговор обаче може да е грешното място за споменаването му. - person Jonathan Grynspan; 26.12.2012
comment
@JonathanGrynspan А, добре, съжалявам за недоразумението. Да, така е. - person ; 26.12.2012
comment
@EricPostpischil Говорим за C. - person ; 26.12.2012
comment
Същият въпрос за C. Въпросният код не променя идентификатор, деклариран като const. Той модифицира обект, деклариран без const, който е бил неправилно свързан с тип, в който обектът е 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-квалифициран обект. Има типове, отговарящи на const. Има правило срещу достъп до обект, дефиниран с тип, квалифициран от const, с lvalue от тип, който не е квалифициран от const, но това няма да се прилага тук, тъй като обектът ще бъде дефиниран с тип, който не е константен, а типът const би да се използва само за достъп. Цитирайте точното правило в C, което е нарушено. - person Eric Postpischil; 27.12.2012
comment
@EricPostpischil Може би не е ясно, но казвам същото, което сте посочили в отговора си. Конкретно имам предвид, че две дефиниции на една и съща структура в различни единици за превод имат част от съвместим тип. - person ; 27.12.2012
comment
[Коригирана печатна грешка.] @H2CO3: Оттегляте ли твърдението си, че „тъй като обектът е квалифициран const и е [модифициран], това извиква UB“? Тъй като обектът не е const-квалифициран (тъй като няма такова нещо) и обектът не е дефиниран от const-квалифициран тип, така че модифицирането му само по себе си не е недефинирано поведение. Ако не го оттегляте, моля, посочете конкретната клауза от стандарта C, която е нарушена. - person Eric Postpischil; 27.12.2012

По думите на Bjarne Stroustrup: 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

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

Винаги заобикаляте указателя към 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