В C++11 защитено означава публично?

Продължаване на нещо научено в C++ грешка: основната функция е защитена ...

Правилата C++11 за указател към член ефективно лишават ключовата дума protected от всякаква стойност, тъй като защитените членове могат да бъдат достъпни в несвързани класове без никакви зли/небезопасни прехвърляния.

За остроумие:

class Encapsulator
{
  protected:
    int i;
  public:
    Encapsulator(int v) : i(v) {}
};

Encapsulator f(int x) { return x + 2; }

#include <iostream>
int main(void)
{
    Encapsulator e = f(7);
    // forbidden: std::cout << e.i << std::endl; because i is protected
    // forbidden: int Encapsulator::*pi = &Encapsulator::i; because i is protected
    // forbidden: struct Gimme : Encapsulator { static int read(Encapsulator& o) { return o.i; } };

    // loophole:
    struct Gimme : Encapsulator { static int Encapsulator::* it() { return &Gimme::i; } };
    int Encapsulator::*pi = Gimme::it();
    std::cout << e.*pi << std::endl;
}

Това наистина поведение съответства ли на стандарта?

(Смятам това за дефект и твърдя, че типът на &Gimme::i наистина трябва да бъде int Gimme::*, въпреки че i е член на базовия клас. Но не виждам нищо в стандарта, което да го прави така, и има много конкретен пример, показващ това.)


Осъзнавам, че някои хора може да са изненадани, че третият коментиран подход (втори тестов случай на ideone) всъщност се проваля. Това е така, защото правилният начин да мислим за защитен не е „моите производни класове имат достъп и никой друг“, а „ако произлизате от мен, вие ще имате достъп до тези наследени променливи, съдържащи се във вашите екземпляри, и никой друг няма, освен ако вие дай го". Например, ако Button наследява Control, тогава защитените членове на Control в рамките на Button екземпляр са достъпни само за Control и Button и (ако приемем, че Button не го забранява) действителния динамичен тип на екземпляра и всички междинни бази.

Тази вратичка подкопава този договор и напълно се противопоставя на духа на правилото 11.4p1:

Допълнителна проверка за достъп извън тези, описани по-рано в Клауза 11, се прилага, когато нестатичен член на данните или нестатична членска функция е защитен член на своя клас за именуване. Както беше описано по-рано, достъпът до защитен член се предоставя, защото препратката се среща в приятел или член на някакъв клас C. Ако достъпът е за формиране на указател към член (5.3.1), спецификаторът на вложено име трябва да обозначава C или клас, получен от C. Всички други достъпи включват (евентуално косвен) обектен израз. В този случай класът на обектния израз трябва да бъде C или клас, получен от C.


Благодаря на AndreyT за връзката http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#203, който предоставя допълнителни примери, мотивиращи промяната, и призовава въпросът да бъде повдигнат от работната група Evolution.


Също подходящо: GotW 76: Употреби и злоупотреби с права за достъп


person Ben Voigt    schedule 06.06.2013    source източник
comment
Да, не виждам каква защита е премахната в C++11.   -  person CB Bailey    schedule 07.06.2013
comment
@Charles: Не исках да предполагам, че C++98 няма същата вратичка. Просто това съществува в текущата версия.   -  person Ben Voigt    schedule 07.06.2013
comment
@BenVoigt: Възможно е също така да ограбите private членове без кастинг   -  person Andy Prowl    schedule 07.06.2013
comment
@Andy: Можете ли да предоставите пример (който не извиква недефинирано поведение)?   -  person Ben Voigt    schedule 07.06.2013
comment
@Ben: Йоханес Шауб измисли този трик. Ето връзката   -  person Andy Prowl    schedule 07.06.2013
comment
@Andy: Това е удивително... и се чудя защо няма проверка на достъпа до &A::a в този контекст.   -  person Ben Voigt    schedule 07.06.2013
comment
@BenVoigt: Нямам представа, но честно казано не мисля, че е толкова лошо (имах предвид факта, че е възможно да се заобиколят правилата за достъпност като се полагат големи усилия). Мисля, че това, което е важно, е, че самият език прави много, за да не ви позволи да нарушите тези правила по погрешка или без да сте добре наясно какво правите.   -  person Andy Prowl    schedule 07.06.2013
comment
@BenVoigt Това е поради 14.7.2 p12 Обичайните правила за проверка на достъпа не се прилагат за имена, използвани за указване на изрични инстанции.   -  person bames53    schedule 07.06.2013
comment
@bames53: Да, видях това правило, но се чудя защо е там.   -  person Ben Voigt    schedule 07.06.2013
comment
@AndyProwl: Не харесвам заобикалянето на правила чрез прилагане на големи усилия. Харесва ми начина, по който работи reinterpret_cast -- той позволява подкопаване на правилата по начин, който е лесен за използване, но много очевиден. Мисля, че ако дизайнерите на езика искат да оставят вратата отворена за заобикаляне на проверките за достъп, например за сериализиране на обекти от класове, синтаксисът private_access( member-access-expression ) би бил много по-добър. Но случаят на използване на сериализация не ме повлиява наистина, тъй като POD обектите могат да бъдат тривиално сериализирани, а тези, които не са POD, изискват помощ от дизайнера на обекти.   -  person Ben Voigt    schedule 07.06.2013


Отговори (2)


Виждал съм тази техника, която наричам „защитен хак“, спомената доста пъти тук и другаде. Да, това поведение е правилно и наистина е законен начин за заобикаляне на защитения достъп, без да се прибягва до „мръсни“ хакове.

Когато m е член на клас Base, тогава проблемът с това, че изразът &Derived::m създава указател от тип Derived::* е, че указателите на член на класа са контравариантни, а не ковариантни. Това би направило получените указатели неизползваеми с Base обекти. Например, този код се компилира

struct Base { int m; };
struct Derived : Base {};

int main() {
  int Base::*p = &Derived::m; // <- 1
  Base b;
  b.*p = 42;                  // <- 2
}

защото &Derived::m произвежда int Base::* стойност. Ако създаде стойност int Derived::*, кодът няма да успее да се компилира на ред 1. И ако се опитаме да го поправим с

  int Derived::*p = &Derived::m; // <- 1

няма да успее да компилира на ред 2. Единственият начин да го накарате да компилира е да извършите принудително кастиране

  b.*static_cast<int Base::*>(p) = 42; // <- 2

което не е добре.

P.S. Съгласен съм, това не е много убедителен пример ("просто използвайте &Base:m от самото начало и проблемът е решен"). Въпреки това, http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#203 има повече информация, която хвърля малко светлина върху това защо първоначално е взето подобно решение. Те заявяват

Бележки от срещата от 04.00:

Обосновката за настоящото третиране е да се позволи възможно най-широко използване на даден израз за адрес на член. Тъй като указател-към-основен-член може да бъде имплицитно преобразуван в указател-към-производен-член, превръщането на типа на израза в указател-към-базов член позволява резултатът да се инициализира или да бъде присвоен или на указател- to-base-member или указател към-derived-member. Приемането на това предложение би позволило само последното използване.

person AnT    schedule 06.06.2013
comment
Но ви позволява да работите с обекти от всеки подклас на Encapsulated, дори тези, които блокират по-нататъшно наследяване! Не мога да си представя, че това е правилно, дори ако е съвместимо. - person Ben Voigt; 07.06.2013
comment
@Ben Voigt: Няма спор, наистина изглежда като огромен компромис. - person AnT; 07.06.2013
comment
WRT вашата редакция, целият смисъл е да направите указателя към член неизползваем с Base обекти. Ако искате указател към член-на-Base, използвайте &Base::m. - person Ben Voigt; 07.06.2013
comment
Разбирам те. Също така вижте тук open-std.org/jtc1/ sc22/wg21/docs/cwg_closed.html#203 . Те имат по-убедителни мотивиращи примери. - person AnT; 07.06.2013
comment
Благодаря за линка. Доколкото мога да преценя, всички тези примери подкрепят промяната (те са нарушени при текущото правило). Така че не съм съгласен с предпоследното ти изречение. Те са само допълнително доказателство, че първоначалното решение не е било добре обмислено. - person Ben Voigt; 07.06.2013
comment
Бих направил допълнителен аргумент по отношение на свързването: Кодът, който присвоява int Base::*p = &Derived::i;, вече е тясно свързан с Base и вече ще се счупи, ако значението на Derived::i се промени (чрез въвеждане на декларация, която скрива члена Base), и така &Base::i не въвеждане на допълнителна тежест за поддръжка. - person Ben Voigt; 07.06.2013

Основното нещо, което трябва да имате предвид относно спецификаторите за достъп в C++ е, че те контролират къде може да се използва име. Всъщност не прави нищо, за да контролира достъпа до обекти. "достъп до член" в контекста на C++ означава "способността да се използва име".

Спазвайте:

class Encapsulator {
  protected:
    int i;
};

struct Gimme : Encapsulator {
    using Encapsulator::i;
};

int main() {
  Encapsulator e;
  std::cout << e.*&Gimme::i << '\n';
}

Това, e.*&Gimme::i, е разрешено, защото изобщо няма достъп до защитен член. Осъществяваме достъп до члена, създаден в Gimme от using декларацията. Тоест, въпреки че using декларация не предполага никакви допълнителни под-обекти в Gimme екземпляри, тя все пак създава допълнителен член. Членовете и подобектите не са едно и също нещо и Gimmie::i е отделен публичен член, който може да се използва за достъп до същите подобекти като защитения член Encapsulator::i.


След като се разбере разграничението между „член на клас“ и „подобект“, трябва да стане ясно, че това всъщност не е вратичка или непреднамерен провал на договора, посочен в 11.4 p1.

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

person bames53    schedule 06.06.2013
comment
Не, спецификаторите за достъп не контролират къде може да се използва дадено име. Разрешаването на претоварване се извършва преди проверките за достъп. И второ, ако &Gimme::i имаше тип int Gimme::*, тогава той все още ще има достъп до Encapsulator::i, но само в рамките на обекти от тип Gimme (или някакъв подклас). Ако вашето тълкуване беше правилно, тогава текстът от 14.4p1, който удебелих във въпроса, нямаше да съществува. - person Ben Voigt; 07.06.2013
comment
@BenVoigt Да, разрешаването на претоварването е на първо място; Под „спецификаторите за достъп контролират къде може да се използва дадено име“ имам предвид, че след като разрешаването на претоварване определи какво име се опитвате да използвате, спецификаторите за достъп определят дали имате право да използвате името. Второ, прочетете отново текста, който сте удебелили, но с разбирането, че „член“ не означава същото като „подобект“; Фразата достъп до защитен член се отнася за името Encapsulator::i, но едновременно с това не се прилага за името Gimmie::i, защото това са отделни членове. Типът &Gimmie::i не влияе на това - person bames53; 07.06.2013
comment
Но типът на &Gimme::i влияе с какви обекти може да се използва указателят по-късно. Напълно съм съгласен, че ако Encapsulator върне указател към своя член подобект, всеки код може след това да дереферира този указател без допълнителна проверка. Но Gimme не трябва да може да получи указател към член, който е общоприложим към всички Encapsulator обекти... за да бъде в съответствие с удебеления текст, &Gimme::i трябва да върне указател, който може да се приложи само към Gimme обекти. - person Ben Voigt; 07.06.2013
comment
@BenVoigt Фактът, че публичният член Gimmie::i може да се използва, не е в противоречие с удебеления текст; удебеленият текст се отнася само за защитени членове. Дали резултатът от използването на този публичен член трябва да бъде използваем с базовия клас или друг клас, получен от същия базов клас, е напълно независимо дизайнерско решение, което не е свързано с удебеления текст. - person bames53; 07.06.2013
comment
Не е в съответствие с удебеления текст да се прави проверка за достъп на Gimme::i и след това да се осъществява достъп до обект, който не е от тип Gimme. - person Ben Voigt; 07.06.2013
comment
Твърдението на вашия отговор, че Gimme::i е различен член (но същия подобект) като Encapsulator::i, просто не работи. &Gimme::i е указател към член (не указател към подобект) и е указател към Encapsulator член, защото няма отделен член Gimme::i. Не е различен, наследен е с променен спецификатор за достъп. - person Ben Voigt; 07.06.2013
comment
Ще призная, че езикът на спецификацията е объркан с използването на термина „член“. Но е ясно, че има различни обекти Gimmie::i и Encapsulator::i, които едновременно имат различни спецификации за достъп. Не е така, че декларацията за използване променя достъпа на някакъв трети обект, общ между двете имена. Ако случаят беше такъв, тогава това трябваше да е законно: Gimmie g; g.*&Encapsulator::i. - person bames53; 07.06.2013
comment
@BenVoigt Не е в съответствие с удебеления текст да се прави проверка на достъпа до Gimme::i и след това да се осъществява достъп до обект, който не е от тип Gimmi. Никъде в спецификацията, да не говорим за цитирания раздел, не се казва, че имената, декларирани в клас, имат достъп само до подобекти на екземпляри от този клас. Спецификацията казва, че проверката на достъпа се извършва върху името и също така, че изразите, използващи име, евентуално могат да произведат стойности, които могат да се използват за достъп до подобекти на екземпляри от напълно различни типове. - person bames53; 07.06.2013
comment
Искате да кажете, че би било законно да напишете struct UseIt : Encapsulator { static void use( Encapsulator& e ) { e.UseIt::i = 1; } };? Но това е забранено от частта от цитата, която удебелих. И за последователност, e.*&UseIt::i = 1; също не трябва да се допуска. - person Ben Voigt; 08.06.2013
comment
Когато казвате, че е ясно, че има различни обекти Gimmie::i и Encapsulator::i, които едновременно имат различни спецификации за достъп, не бихте ли казали, че &Gimmie::i трябва да бъде указател към член, който има достъп до първия? И следователно може да се използва само с обект, който съдържа Gimmie::i член? Проблемът е, че те не са различни обекти. &Gimmie::i създава указател към Encapsulator::i. - person Ben Voigt; 08.06.2013
comment
@BenVoigt Искате да кажете, че би било законно да напишете [...]? Не, и не заради нещо в цитираното твърдение. Незаконно е, защото . трябва дясната страна да бъде името на член от лявата страна, а UseIt::i не е името на член на Encapsulator. .* от друга страна позволява дясната страна да бъде всеки израз, който е от подходящ тип. - person bames53; 08.06.2013
comment
Проблемът е, че те не са отделни единици. &Gimmie::i създава указател към Encapsulator::i Изразът &Gimmie::i случайно има същия тип и стойност като израза &Encapsulator::i, точно както израз f() може да има същия тип и стойност като израз g(). Това не означава, че id-израз Gimmie::i е същият като id-израз Encapsulator::i повече от g и f трябва да са еднакви. - person bames53; 08.06.2013
comment
В C++, ако два израза имат един и същи тип и адрес, това означава, че са еднакви. Не говорим за стойност като цяло, а за адрес. - person Ben Voigt; 09.06.2013
comment
@BenVoigt Ако два обекта имат един и същи тип и адрес, те са един и същ обект. Не може да се приложи една и съща логика, за да се заключи, че Gimmie::i и Encapsulator::i са едни и същи само защото прилагането на така наречения оператор „адрес на“ дава един и същ адрес и за двата; Те не са обекти и указателите към членове не са всъщност адреси, каквито са указателите към обекти. Както и да е, вече идентифицирахме няколко начина, по които Gimmie::i и Encapsulator::i се държат различно и следователно не трябва да са еднакви. - person bames53; 09.06.2013