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

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

#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 (дажеic), с 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

Solution # 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 : Отключите строгое соответствие стандарту C ++, включив «разрешающий» режим в настройках компилятора, например:

Для MSVC v19.10 снимите переключатель /permissive-,см .: https://godbolt.org/g/Yxw89Y
Для ICC v18.0.0 добавить переключатель -fpermissive, см .: https://godbolt.org/g/DwuTb4
Для GCC v8.1 добавить переключатель -fpermissive, see: https://godbolt.org/g/DHGBpW

MSVC ПРИМЕЧАНИЕ. Согласно этой статье , по умолчанию параметр /permissive- установлен в новых проектах, созданных Visual Studio2017 v15.5 (компилятор MSVC v19.11) и более поздние версии.Он не устанавливается по умолчанию в более ранних версиях, включая последнюю версию MSVC Compiler Explorer от Godbolt.org v19.10.

GCC ПРИМЕЧАНИЕ. Даже с переключателем компилятора -fpermissiveкомпилятору GCC v8.1 по-прежнему нужен оператор using CBase<BYTES>::Arr; внутри класса CDerived (... или одно из других решений), чтобы сделать публичный массив Arr видимым внутри шаблона класса CDerived.... но не нужно ничего дополнительного, чтобы сделать видимыми функции Fn1() и Fn2().

MSVC Non-Solution : Согласно В этой статье и В этой статье ошибка компиляции в MSVC возникает из-за двухфазного поиска имен, включаемого в соответствии со стандартным режимом C ++ (*Опция 1130 *).
Кроме того, согласно бывшей статье : " Опция /permissive- неявно устанавливает соответствующее поведение компилятора двухфазного поиска, но его можно переопределить с помощью /Zc:twoPhase- switch".
Однако добавление двухпереключение компилятора /permissive- /Zc:twoPhase- не приводит к компиляции «шаблонного» кода проблемы в MSVC v19.14 без дополнений, описанных в решении № 1, № 2 или № 3.

MSVC v19.14: https://godbolt.org/z/BJlyA8

Подробнее см. в этой записи .

Проблемы с вышеуказанными решениями :
Решение № 4не переносимый и отрывается от стандарта C ++.Это также ГЛОБАЛЬНОЕ решение (глобальное переключение) для локальной проблемы - обычно плохая идея.Переключатель компилятора, который влияет только на часть кода (например, #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 функций и множество переменных, на которые ссылаются несколько раз в шаблоне производного класса., что требует сотен таких повторных правок!
Должен быть лучший способ ...

Решение № 3 требует меньше работы, поскольку не требует поиска и префикса КАЖДОЙ ССЫЛКИ к элементу CBase в коде CDerived.CBase члены, которые используются CDerived, должны быть "повторно объявлены" с помощью оператора using только один раз , независимо от того, сколько раз эти члены используются / упоминаются в CDerived код.Это экономит много бессмысленного поиска и ввода.

К сожалению, такого общего оператора, как using CBase<BYTES>::*, который делает все защищенные и открытые члены видимыми в шаблоне производного класса, не существует.

ВОПРОС :
Есть ли менее подробное портативное решение этой проблемы?например, решение № 5 ...

Ответы [ 3 ]

0 голосов
/ 14 мая 2018

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

#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);
0 голосов
/ 25 марта 2019

Я слышал, что именно эта проблема обсуждалась в CppCon 2018, и комитет C ++ планирует добавить специальные ключевые слова, чтобы упростить эти повторяющиеся объявления и избежать необходимости ставить префикс всех унаследованных членов с ключевым словом this->.

В вашем случае это будет:
using CBase<BYTES>::all_public

Также планируется новое ключевое слово all_protected.Это ключевое слово будет наследовать только защищенные члены.Ключевое слово all_public уже наследует все защищенные элементы, в дополнение к общедоступным.

0 голосов
/ 14 мая 2018

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...