Контекст:
Наследование защищенных и публичных членов класса является фундаментальной концепцией объектно-ориентированного программирования. Тривиальный пример ниже иллюстрирует часто встречающуюся ситуацию, в которой класс CDerived
наследует всех публичных членов класса CBase
и добавляет 1 дополнительную функцию из своего собственного без изменений, а также явно переопределяет или переопределяет любой из публичные члены класса CBase
.
#include <stdio.h>
class CBase
{
public:
char Arr[32];
int Fn1(void) {
return Arr[1] ^ Arr[sizeof(Arr)-1];
}
int Fn2(void) {
return Arr[2] ^ Arr[sizeof(Arr)-2];
}
};
class CDerived : public CBase
{
public:
int FnSum(void) {
return Fn1() + Fn2();
}
};
int main(void)
{
CDerived ddd;
printf("%d\n", ddd.Fn1());
printf("%d\n", ddd.Fn2());
printf("%d\n", ddd.FnSum());
return (int)ddd.Arr[0];
};
Код выше компилируется без проблем во всех основных компиляторах.
Однако, если вы хотите "templatize" этого кода, например: путем параметризации размера массива Arr
, тогда все публичные члены шаблона класса CBase
становятся невидимыми для шаблона класса CDerived
для компиляторов, которые соответствуют последнему стандарту С++.
Ниже приведен код проблемы:
#include <stdio.h>
template <unsigned int BYTES>
class CBase
{
public:
char Arr[BYTES];
int Fn1(void) {
return Arr[1] ^ Arr[sizeof(Arr)-1];
}
int Fn2(void) {
return Arr[2] ^ Arr[sizeof(Arr)-2];
}
};
template <unsigned int BYTES>
class CDerived : public CBase<BYTES>
{
public:
int FnSum(void) {
return Fn1() + Fn2() + Arr[0]; // ERRORs: identifiers "Fn1" and "Fn2" and "Arr" are NOT found !
}
};
int main(void)
{
CDerived<32> ddd;
printf("%d\n", ddd.Fn1()); //No error here
printf("%d\n", ddd.Fn2()); //No error here
printf("%d\n", ddd.FnSum());
return (int)ddd.Arr[0]; //No error here
}
См:
MSVC v19.10: https://godbolt.org/g/eQKDhb
ICC v18.0.0: https://godbolt.org/g/vBBEQC
GCC v8.1: https://godbolt.org/g/GVkeDh
Существует 4 решения этой проблемы:
Решение # 1: префикс всех ссылок на элементы шаблона класса CBase
(даже публичные), с CBase<BYTES>::
следующим образом:
int FnSum(void) {
return CBase<BYTES>::Fn1() + CBase<BYTES>::Fn2() + CBase<BYTES>::Arr[0];
}
См:
MSVC v19.10: https://godbolt.org/g/48ZJrj
ICC v18.0.0: https://godbolt.org/g/BSPcSQ
GCC v8.1: https://godbolt.org/g/Vg4SZM
Решение # 2: префикс всех ссылок на элементы шаблона класса CBase
(даже публичные), с this->
следующим образом:
int FnSum(void) {
return this->Fn1() + this->Fn2() + this->Arr[0];
}
См:
MSVC v19.10: https://godbolt.org/g/oBs6ud
ICC v18.0.0: https://godbolt.org/g/CWgJWu
GCC v8.1: https://godbolt.org/g/Gwn2ch
Решение № 3. Добавьте один оператор using
внутри шаблона класса CDerived
для каждого члена CBase
(даже публичного), на который ссылается CDerived
, вот так:
using CBase<BYTES>::Arr;
using CBase<BYTES>::Fn1;
using CBase<BYTES>::Fn2;
См:
MSVC v19.10: https://godbolt.org/g/gJT8cX
ICC v18.0.0: https://godbolt.org/g/1RK84A
GCC v8.1: https://godbolt.org/g/d8kjFh
Решение № 4. Отключите строгое соответствие стандарту С++, включив режим "разрешительный" в настройках компилятора, например:
Для MSVC v19.10 удалите переключатель /permissive-
, см. https://godbolt.org/g/Yxw89Y
Для ICC v18.0.0 добавьте переключатель -fpermissive
, см. https://godbolt.org/g/DwuTb4
Для GCC v8.1 добавьте переключатель -fpermissive
, см. https://godbolt.org/g/DHGBpW
MSVC ПРИМЕЧАНИЕ. Согласно этой статье, по умолчанию параметр /permissive-
задается в новых проектах созданный Visual Studio 2017 v15.5 (MSVC-компилятор v19.11) и более поздние версии. Он не задан по умолчанию в более ранних версиях,... включая последнюю версию MSVC для разработчиков компилятора Godbolt.org v19.10.
GCC ПРИМЕЧАНИЕ. Даже с помощью компилятора -fpermissive
компилятору GCC v8.1 по-прежнему нужен оператор using CBase<BYTES>::Arr;
внутри класса CDerived
(... или одного из других решений), чтобы сделать public Arr
, видимый внутри шаблона класса CDerived
... но ему не нужно ничего лишнего, чтобы сделать функции Fn1()
и Fn2()
видимыми.
MSVC Non-Solution:
Согласно эта статья и этой статьи, ошибка компиляции в MSVC происходит из двухфазного поиска по имени, которая включена в соответствии со стандартным режимом С++ (опция /permissive-
).
Кроме того, в соответствии с прежняя статья: "Опция /permissive-
неявно устанавливает согласованное двухфазное поведение компилятора поиска, но ее можно переопределить с помощью /Zc:twoPhase-
switch".
Однако добавление двух компиляторов /permissive- /Zc:twoPhase-
не приводит к компиляции "проблемного" кода проблемы в MSVC v19.13 без добавления, описанного в решении №1 или №2 или №3. Подробнее см. эту запись.
ПРИМЕЧАНИЕ. Я не включаю ссылку на http://godbolt.orgс иллюстрацией этой проблемы, поскольку последний компилятор MSVC в Godbolt (v19.10) не поддерживает компилятор /Zc:twoPhase-
.
Проблемы с вышеуказанными решениями:
Решение №4 не переносимо и отрывается от стандарта С++. Это также GLOBAL-решение (глобальный коммутатор) для локальной проблемы - обычно это плохая идея. Коммутатор компилятора, который затрагивает только часть кода (например, #pragma NOtwoPhase
), не существует.
Решение № 1 имеет непреднамеренный побочный эффект подавления виртуальных вызовов, поэтому в общем случае он неприменим.
Оба решения # 1 и # 2 требуют много подробных дополнений к коду. Это приводит к разрастанию исходного кода, который не добавляет никаких новых функций. Например, если шаблон класса CDerived
добавляет только 2 функции к классу CBase
, который содержит 5 открытых функций и 1 переменную-член, на которые многократно ссылаются в CDerived
, для решения № 1 требуется 14 подробных кодов изменения/дополнения в производном классе, которые выглядят следующим образом:
#include <stdio.h>
template <unsigned int BYTES>
class CBase
{
public:
char Arr[BYTES];
CBase() {
for (size_t i=1; i<sizeof(Arr); i++)
Arr[i] = Arr[i-1]+(char)i;
}
int Fn1(void) {
return Arr[1] ^ Arr[sizeof(Arr)-1];
}
int Fn2(void) {
return Arr[2] ^ Arr[sizeof(Arr) - 2];
}
int Fn3(void) {
return Arr[3] ^ Arr[sizeof(Arr) - 3];
}
int Fn4(void) {
return Arr[4] ^ Arr[sizeof(Arr) - 4];
}
int Fn5(void) {
return Arr[5] ^ Arr[sizeof(Arr) - 5];
}
};
template <unsigned int BYTES>
class CDerived : public CBase<BYTES>
{
public:
int FnSum(void) {
return CBase<BYTES>::Fn1() +
CBase<BYTES>::Fn2() +
CBase<BYTES>::Fn3() +
CBase<BYTES>::Fn4() +
CBase<BYTES>::Fn5() +
CBase<BYTES>::Arr[0] +
CBase<BYTES>::Arr[1] +
CBase<BYTES>::Arr[2];
}
int FnProduct(void) {
return CBase<BYTES>::Fn1() *
CBase<BYTES>::Fn2() *
CBase<BYTES>::Fn3() *
CBase<BYTES>::Fn4() *
CBase<BYTES>::Fn5() *
CBase<BYTES>::Arr[0] *
CBase<BYTES>::Arr[1] *
CBase<BYTES>::Arr[2];
}
};
int main(void)
{
CDerived<32> ddd;
printf("%d\n", ddd.FnSum());
printf("%d\n", ddd.FnProduct());
return (int)ddd.Arr[0];
}
В реальной жизни шаблон базового класса может содержать ~ 50 функций и множество переменных, которые несколько раз ссылаются на шаблон класса Derived, что требует 100 таких повторных изменений!
Должен быть лучший способ...
Решения №3 требуют меньше работы, потому что не требуют поиска и префикса КАЖДОЙ ССЫЛКИ к члену CBase
в коде CDerived
. Члены CBase
, которые используются CDerived
, должны быть "повторно объявлены" с помощью оператора using
только один раз, независимо от того, сколько раз эти элементы используются/ссылаются в CDerived
код. Это экономит много бессмысленного поиска и ввода текста.
К сожалению, общий оператор типа using CBase<BYTES>::*
, который делает все защищенные и публичные элементы видимыми в шаблоне производного класса, не существует.
Вопрос:
Есть ли менее подробное портативное решение этой проблемы? например Решение № 5...
P.S. Этот вопрос связан с этим вопросом.