使用继承的类模板避免公共成员不可见和源代码膨胀/重复的更好方法?

时间:2023-02-23
本文介绍了使用继承的类模板避免公共成员不可见和源代码膨胀/重复的更好方法?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着跟版网的小编来一起学习吧!

问题描述

上下文:
protected 和 public 类成员的继承是面向对象编程的一个基本概念.下面这个简单的例子说明了一个经常遇到的情况,其中 CDerived 类继承了 CBase 类的所有公共成员,并添加了自己的 1 个附加函数没有strong> 更改或明确重新声明或重新定义 CBase 类的任何公共成员.

Context:
Inheritance of protected and public class members is a fundamental concept of Object Oriented Programming. The trivial example below illustrates an often encountered situation in which the class CDerived inherits all public members of the class CBase and adds 1 additional function of its own without changing nor explicitly redeclaring nor redefining any of the public members of the CBase class.

#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
", ddd.Fn1());
    printf("%d
", ddd.Fn2());
    printf("%d
", ddd.FnSum());

    return (int)ddd.Arr[0];
};

以上代码在所有主要编译器上都没有问题.

The code above compiles without problems on all major compilers.

然而,如果希望模板化"这个代码,例如:通过参数化Arr数组的大小,那么所有公共成员 CBase 类模板的在符合最新 C++ 标准的编译器上对 CDerived 类模板不可见.
下面是问题代码:

However, if one wishes to "templatize" this code, e.g.: by parametrizing the size of the Arr array, then all public members of CBase class template become invisible to the CDerived class template on compilers that conform to the latest C++ standard.
Below is the problem code:

#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
", ddd.Fn1());  //No error here
    printf("%d
", ddd.Fn2());  //No error here
    printf("%d
", 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种解决方案:

There are 4 solutions to this problem:

解决方案 #1:在对 CBase 类模板(甚至是公共模板)成员的所有引用前加上 CBase:: 像这样:

Solution #1: Prefix all of the references to members of the CBase class template (even the public ones), with CBase<BYTES>:: like this:

 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->代码>像这样:

Solution #2: Prefix all of the references to members of the CBase class template (even the public ones), with this-> like 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:在CDerived 类模板中为CBaseusing 语句>(甚至是公共的)被 CDerived 引用,像这样:

Solution #3: Add one using statement inside the CDerived class template, for each member of the CBase (even a public one) that is referenced by the CDerived, like this:

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++ 标准的严格一致性,如下所示:

Solution #4: Disable the strict conformance to the C++ standard by enabling the "permissive" mode in the compiler settings, like this:

对于 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

For MSVC v19.10 remove the switch /permissive-, see: https://godbolt.org/g/Yxw89Y
For ICC v18.0.0 add the switch -fpermissive, see: https://godbolt.org/g/DwuTb4
For GCC v8.1 add the switch -fpermissive, see: https://godbolt.org/g/DHGBpW

MSVC 注意:根据 本文,默认情况下,/permissive- 选项在 Visual Studio 2017 v15.5(MSVC 编译器 v19.11)及更高版本创建的新项目中设置.它在早期版本中没有默认设置,...包括最新的 Godbolt.org 的编译器资源管理器 MSVC 版本 v19.10.

MSVC NOTE: According to this article, by default the /permissive- option is set in new projects created by Visual Studio 2017 v15.5 (MSVC compiler v19.11) and later versions. It is not set by default in earlier versions, ...including the latest Godbolt.org's Compiler Explorer MSVC version v19.10.

GCC 注意:即使使用 -fpermissive 编译器开关,GCC v8.1 编译器仍然需要 using CBase::Arr; 语句在 CDerived 类(...或其他解决方案之一)中,以使公共 Arr 数组在 CDerived 类模板中可见...但它不需要任何额外的东西来使 Fn1()Fn2() 函数可见.

GCC NOTE: Even with the -fpermissive compiler switch, the GCC v8.1 compiler still needs the using CBase<BYTES>::Arr; statement inside the CDerived class (...or one of the other solutions) in order to make the public Arr array visible inside the CDerived class template ...but it does not need anything extra to make the Fn1() and Fn2() functions visible.

MSVC 非解决方案:根据这篇文章本文,MSVC中的编译错误来自于符合C++标准模式(/permissive-选项)启用的两阶段名称查找.
此外,根据前一篇文章:/permissive- 选项隐式设置了符合两阶段查找的编译器行为,但它可以通过使用 /Zc:twoPhase- 开关来覆盖.
然而,添加两个编译器开关 /permissive-/Zc:twoPhase- 不会导致模板化"问题代码在 MSVC v19.14 中编译,没有解决方案 #1 或 #2 中描述的添加或#3.

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

MSVC Non-Solution: According to this article and this article, the compilation error in MSVC comes from the Two-Phase Name Lookup being enabled by the conformance to the C++ standard mode ( the /permissive- option).
Also, according to the former article: "The /permissive- option implicitly sets the conforming two-phase lookup compiler behavior, but it can be overridden by using /Zc:twoPhase- switch".
However adding the two compiler switches /permissive- /Zc:twoPhase- does not cause the "templated" problem code to compile in MSVC v19.14, without the additions described in Solution #1 or #2 or #3.

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

参见此条目 了解更多详情.

See this entry for more details.

上述解决方案的问题:
解决方案#4 不可移植并且脱离了 C++ 标准.它也是针对本地问题的全局解决方案(全局切换)——通常是一个坏主意.不存在仅影响部分代码的编译器开关(例如 #pragma NOtwoPhase).
解决方案#1 具有抑制虚拟调用的意外副作用,因此它不适用于一般情况.
解决方案#1 和#2 都需要对代码进行许多详细的添加.这会导致源代码膨胀,但不会添加任何新功能.例如,如果 CDerived 类模板仅向包含 5 个公共函数和 1 个成员变量的 CBase 类添加了 2 个函数,这些函数在 CDerived,解决方案 #1 需要在派生类中进行 14 处详细代码更改/添加,如下所示:

Problems with above Solutions:
Solution #4 is not portable and breaks away from the C++ standard. It is also a GLOBAL solution (global switch) to a local problem - usually a bad idea. A compiler switch that affects only a portion of the code (e.g. #pragma NOtwoPhase) does not exist.
Solution #1 has an unintended side-effect of suppressing virtual calls, thus it is not applicable in general case.
Both solutions #1 and #2 require many verbose additions to the code. This leads to a source code bloat that does not add any new functionality. For example if the CDerived class template adds only 2 functions to a CBase class that contains 5 public functions and 1 member variable, which are referenced multiple times in CDerived, the Solution #1 requires 14 verbose code alterations/additions in the derived class, which look like this:

    #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
", ddd.FnSum());
        printf("%d
", ddd.FnProduct());

        return (int)ddd.Arr[0];
    }

在现实生活中,基类模板可能包含约 50 个函数和许多在派生类模板中多次引用的变量,这需要 100 次这样的重复编辑!
一定有更好的方法...

In real life the Base class template might contain ~50 functions and many variables which are referenced multiple times in the Derived class template, which necessitate 100s of such repetitive edits !
There must be a better way...

解决方案#3 需要较少的工作,因为它不需要在 CDerived 的代码中查找和添加对 CBase 成员的每个 REFERENCE 的前缀.CDerived 使用的 CBase 成员需要使用 using 语句重新声明"仅一次,无论这些成员在 CDerived 的代码中被使用/引用多少次.这样可以节省大量无意识的搜索和输入.

Solution #3 requires less work because it does not require finding and prefixing EVERY REFERENCE to the CBase member in the CDerived's code. The CBase members, that are used by CDerived, need to be "re-declared" with a using statement only once, regardless how many times these members are used/referenced in the CDerived's code. This saves a lot of mindless searching and typing.

不幸的是,像 using CBase<BYTES>::* 这样的一揽子声明不存在,它使派生类模板中的所有受保护和公共成员都可见.

Unfortunately a blanket statement like using CBase<BYTES>::* which makes all of the protected and public members visible in the derived class template, does not exist.

问题:
是否有针对此问题的不那么冗长的便携式解决方案?例如解决方案 #5...

QUESTION:
Is there a less verbose portable solution to this problem ? e.g. Solution #5...

推荐答案

冒着被否决的风险,我打算冒险不回答你的问题.事实上,我要反其道而行之,说整个努力从一开始就被误导了.

At the risk of getting downvoted, I'm going to go on a limb and intentionally not answer your question. In fact I am going to do the opposite and say that the whole endeavor is misguided from the get-go.

像您所描述的场景类型,其中子类调用方法或引用其父类的成员,除了少数特定情况外,被认为是不良代码.如果您想阅读有关该反模式的更多信息,则将其称为 inherit-to-extend.很好的 SO 答案作为该主题的介绍

The type of scenarios like you describe, where a child class invokes methods or refer to members of its parent class is, with the exception of a few specific cases, considered bad code. It's called inherit-to-extend if you want to read more on that anti-pattern. Good SO answer as an intro on the subject

好吧,它并不是糟糕的代码,因为它是一种代码异味:一种模糊的迹象,表明代码的基本设计中有些地方不太正确.

Ok, well it's not so much bad code, as it is a code smell: a vague indication that something is not quite right in the fundamental design of the code.

代码异味是可以的,您不必特意避开每一种异味,而且您所描述的模式在您的情况下可能确实是正确的做法.然而,这将是 naughty 代码,值得一个大注释块来解释为什么在这种情况下它是可以的.

Code smells are ok, you don't necessarilly have to go out of your way to avoid every single one of them, and the pattern you described might genuinely be the right thing to do in your case. However, it would be naughty code, that deserves a big comment block to explain why it's ok in this instance.

为了更轻松地编写顽皮代码而跳槽是个坏主意.

Jumping through hoops to make it easier to write naughty code is just a bad idea.

这篇关于使用继承的类模板避免公共成员不可见和源代码膨胀/重复的更好方法?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!