Лучший способ избежать невидимости публичного участника и разрастания/повторения исходного кода с унаследованными шаблонами классов?

Контекст:
Наследование защищенных и публичных членов класса является фундаментальной концепцией объектно-ориентированного программирования. Тривиальный пример ниже иллюстрирует часто встречающуюся ситуацию, в которой класс 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. Этот вопрос связан с этим вопросом.

Ответ 1

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

#include <boost/preprocessor.hpp>

#define USING_ONE(r, base, member)              \
    using base::member;

#define USING_ALL(base, ...)                    \
    BOOST_PP_SEQ_FOR_EACH(                      \
        USING_ONE, base,                        \
        BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)   \
    )

// Near CBase<BYTES>
#define USING_CBASE(param) USING_ALL(CBase<param>, Arr, Fn1, Fn2, Fn3, Fn4, Fn5)

// In CDerived<BYTES>, in a `public:` section
USING_CBASE(BYTES);

Ответ 2

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

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

Хорошо, это не очень плохой код, так как это запах кода: неопределенное указание на то, что в фундаментальном дизайне кода что-то не совсем правильно.

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

Прыжки через обручи, чтобы упростить создание непослушного кода - это просто плохая идея.