【问题标题】:Partial class definition on C++?C ++上的部分类定义?
【发布时间】:2010-09-13 13:06:16
【问题描述】:

任何人都知道是否可以在 C++ 上定义部分类?

类似:

file1.h:

类测试{ 民众: int test1(); };

文件2.h:

类测试{ 民众: int test2(); };

对我来说,定义多平台类似乎非常有用,这些类在它们之间具有独立于平台的通用功能,因为继承是一种对多平台类没有用处的成本。

我的意思是你永远不会在运行时有两个多平台专业化实例,只有在编译时。继承可能有助于满足您的公共接口需求,但之后它不会在运行时添加任何有用的东西,只会增加成本。

此外,您将不得不使用丑陋的#ifdef 来使用该类,因为您无法从抽象类创建实例:

类通用测试 { 民众: int 通用方法(); };

那么让我们说win32:

类win32Test:公共通用测试{ 民众: int win32Method(); };

也许:

类macTest:公共genericTest { 民众: int macMethod(); };

假设 win32Method() 和 macMethod() 都调用 genericMethod(),你将不得不像这样使用类:

#ifdef _WIN32 genericTest *test = new win32Test(); #elif MAC genericTest *test = new macTest(); #万一 测试->通用方法();

现在想了一会儿,继承只对为它们提供一个依赖于特定平台的 genericMethod() 有用,但因此需要调用两个构造函数。此外,您的代码周围散布着丑陋的#ifdef。

这就是我寻找部分课程的原因。我可以在编译时定义特定平台相关的部分结束,当然,在这个愚蠢的例子中,我仍然需要在 genericMethod() 中使用丑陋的#ifdef,但还有另一种方法可以避免这种情况。

【问题讨论】:

  • 首先,非虚拟函数不会产生“成本”,其次,如果您的代码未指明您使用的是哪个版本的接口,则在一个版本上工作会破坏另一个版本,例如当你做 test->win32Method().特定于平台的类不一样,应该有不同的名称。
  • 您可以使用工厂方法来最小化#ifdefs。并且您可以使用模板 (en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern) 进一步减少它。
  • C++ 是如此古老而糟糕的语言((
  • Microsoft 的 C++/CX 支持带有非标准关键字的部分类,partialref,就在 class 关键字之前,这是它的 document。我宁愿避免使用这个功能,因为它不是标准的 C++。

标签: c++


【解决方案1】:

这在 C++ 中是不可能的,它会给你一个关于重新定义已经定义的类的错误。如果您想共享行为,请考虑继承。

【讨论】:

    【解决方案2】:

    尝试继承

    具体

    class AllPlatforms {
    public:
        int common();
    };
    

    然后

    class PlatformA : public AllPlatforms {
    public:
        int specific();
    };
    

    【讨论】:

      【解决方案3】:

      您不能在 C++ 中部分定义类。

      这是一种获得“多态性,其中只有一个子类”效果的方法,无需开销,并且只需最少的#define 或代码重复。这叫做模拟动态绑定:

      template <typename T>
      class genericTest {
      public:
          void genericMethod() {
              // do some generic things
              std::cout << "Could be any platform, I don't know" << std::endl;
              // base class can call a method in the child with static_cast
              (static_cast<T*>(this))->doClassDependentThing();
          }
      };
      
      #ifdef _WIN32
          typedef Win32Test Test;
      #elif MAC
          typedef MacTest Test;
      #endif
      

      然后在其他一些标题中关闭:

      class Win32Test : public genericTest<Win32Test> {
      public:
          void win32Method() {
              // windows-specific stuff:
              std::cout << "I'm in windows" << std::endl;
              // we can call a method in the base class
              genericMethod();
              // more windows-specific stuff...
          }
          void doClassDependentThing() {
              std::cout << "Yep, definitely in windows" << std::endl;
          }
      };
      

      class MacTest : public genericTest<MacTest> {
      public:
          void macMethod() {
              // mac-specific stuff:
              std::cout << "I'm in MacOS" << std::endl;
              // we can call a method in the base class
              genericMethod();
              // more mac-specific stuff...
          }
          void doClassDependentThing() {
              std::cout << "Yep, definitely in MacOS" << std::endl;
          }
      };
      

      这会在编译时为您提供适当的多态性。 genericTest 可以非虚拟方式调用 doClassDependentThing 以提供平台版本(几乎类似于虚拟方法),当 win32Method 调用 genericMethod 时,它当然会获取基类版本。

      这不会产生与虚拟调用相关的开销 - 您可以获得与键入两个没有共享代码的大类相同的性能。它可能会在 con(de)struction 时创建非虚拟调用开销,但如果 genericTest 的 con(de)structor 是内联的,你应该没问题,而且在任何情况下,开销并不比具有由调用的 genericInit 方法差两个平台。

      客户端代码只是创建 Test 实例,并且可以在它们上调用 genericTest 或平台正确版本中的方法。为了帮助不关心平台并且不想意外使用特定于平台的调用的代码中的类型安全,您还可以执行以下操作:

      #ifdef _WIN32
          typedef genericTest<Win32Test> BaseTest;
      #elif MAC
          typedef genericTest<MacTest> BaseTest;
      #endif
      

      在使用 BaseTest 时必须小心谨慎,但不会比 C++ 中的基类更谨慎。例如,不要用判断错误的传递值来分割它。并且不要直接实例化它,因为如果你这样做并调用一个最终尝试“假虚拟”调用的方法,你就有麻烦了。后者可以通过确保所有 genericTest 的构造函数都受到保护来强制执行。

      【讨论】:

      • 我想你也有同样的想法,我喜欢它。
      • 您应该能够通过搜索“模拟动态绑定”或“静态多态性”找到更多提示和技巧。我不确定我是否曾经认真使用过它,因为在实践中,虚拟调用的开销实际上很少见。不过,如果有的话,我会准备好的:-)
      • 我建议将我们的Polymorph abstraction 用于任何多态性,包括静态的和可选的动态的。
      【解决方案4】:

      或者你可以试试 PIMPL

      常用头文件:

      class Test
      {
      public:
          ...
          void common();
          ...
      private:
          class TestImpl;
          TestImpl* m_customImpl;
      };
      

      然后创建 cpp 文件来执行特定于平台的自定义实现。

      【讨论】:

        【解决方案5】:
        #include will work as that is preprocessor stuff.
        
        class Foo
        {
        #include "FooFile_Private.h"
        }
        
        ////////
        
        FooFile_Private.h:
        
        private:
          void DoSg();
        

        【讨论】:

          【解决方案6】:

          这个怎么样:

          class WindowsFuncs { public: int f(); int winf(); };
          class MacFuncs { public: int f(); int macf(); }
          
          class Funcs
          #ifdef Windows 
              : public WindowsFuncs
          #else
              : public MacFuncs
          #endif
          {
          public:
              Funcs();
              int g();
          };
          

          现在Funcs 是一个在编译时已知的类,因此抽象基类或其他任何东西都不会造成开销。

          【讨论】:

          • 你在这里做什么,你可以使用模板和静态多态性来做。例如,看看我们的Polymorph type,它具有用于在使用类之前重新定义任意数量的基类型的类型参数。
          【解决方案7】:

          正如写的那样,这是不可能的。

          您可能想查看命名空间。您可以将函数添加到另一个文件中的命名空间。类的问题是每个 .cpp 都需要查看类的完整布局。

          【讨论】:

            【解决方案8】:

            没有。

            但是,您可能想查找一种称为“策略类”的技术。基本上,您制作微课程(它们本身没有用),然后在稍后将它们粘合在一起。

            【讨论】:

            【解决方案9】:

            如 Jamie 所说,要么使用继承,要么使用 #ifdef 使不同的部分在不同的平台上编译。

            【讨论】:

              【解决方案10】:

              对我来说,定义多平台类似乎非常有用,这些类在它们之间具有独立于平台的通用功能。

              除非开发人员几十年来一直在这样做,但没有此“功能”。

              我认为创建 partial 是因为微软几十年来也有生成代码并将其交给开发人员开发和维护的坏习惯。

              生成的代码通常是维护的噩梦。当您需要提升 MFC 版本时,整个 MFC 生成的框架有哪些习惯?或者在升级 Visual Studio 时如何将所有代码移植到 *.designer.cs 文件中?

              大多数其他平台更多地依赖生成配置文件,而不是用户/开发人员可以修改。那些,词汇量比较有限,不容易与不相关的代码混在一起。如果认为有必要,配置文件甚至可以作为资源文件插入到二进制文件中。

              我从未见过在继承或配置资源文件不能做得更好的地方使用“部分”。

              【讨论】:

              • 如果你有一个模板接口类,你从它继承了 CRTP-fashion,它也被另一个具有相同特化的模板类调用,并且从接口派生的类使用 pImpl 成语有所有公共功能的实现相同,但所有私有功能必须具有不同的特定于平台的实现?与基于 #ifdef 开关的公共接口实现之上包含单独的 Impl 文件相比,部分类在这里会更干净。
              • 回复很晚,但是对于每个指令形式具有单独功能的模拟器会更容易使用部分类的 IMO。如果您将所有声明放在一个文件中(C++ 需要),您将在 一个文件中拥有 1000s 个函数定义! 对于 Bochs,请查看 @ 的末尾987654322@:sourceforge.net/p/bochs/code/HEAD/tree/trunk/bochs/cpu/cpu.h。如果部分类是一个东西,您可以将这些定义分开。虽然没有必要,但它会大大缩小文件。
              【解决方案11】:

              由于标题只是以文本形式插入,因此其中一个可以省略“class Test {”和“}”并#included 在另一个中间。

              我实际上已经在生产代码中看到了这一点,尽管 Delphi 不是 C++。这让我特别恼火,因为它破坏了 IDE 的代码导航功能。

              【讨论】:

              • 这是非常错误的,非常错误的,但我认为是一个有效的解决方案:-)
              【解决方案12】:

              肮脏但实用的方法是使用#include预处理器:

              测试.h:

              #ifndef TEST_H
              #define TEST_H
              
              class Test
              {
              public:
                  Test(void);
                  virtual ~Test(void);
              
              #include "Test_Partial_Win32.h"
              #include "Test_Partial_OSX.h"
              
              };
              
              #endif // !TEST_H
              

              Test_Partial_OSX.h:

              // This file should be included in Test.h only.
              
              #ifdef MAC
                  public:
                      int macMethod();
              #endif // MAC
              

              Test_Partial_WIN32.h:

              // This file should be included in Test.h only.
              
              #ifdef _WIN32
                  public:
                      int win32Method();
              #endif // _WIN32
              

              Test.cpp:

              // Implement common member function of class Test in this file.
              
              #include "stdafx.h"
              #include "Test.h"
              
              Test::Test(void)
              {
              }
              
              Test::~Test(void)
              {
              }
              

              Test_Partial_OSX.cpp:

              // Implement OSX platform specific function of class Test in this file.
              
              #include "stdafx.h"
              #include "Test.h"
              
              #ifdef MAC
              int Test::macMethod()
              {
                  return 0;
              }
              #endif // MAC
              

              Test_Partial_WIN32.cpp:

              // Implement WIN32 platform specific function of class Test in this file.
              
              #include "stdafx.h"
              #include "Test.h"
              
              #ifdef _WIN32
              int Test::win32Method()
              {
                  return 0;
              }
              #endif // _WIN32
              

              【讨论】:

                【解决方案13】:

                假设我有:

                MyClass_Part1.hpp、MyClass_Part2.hpp 和 MyClass_Part3.hpp

                理论上有人可以开发一个 GUI 工具来读取上述所有这些 hpp 文件并创建以下 hpp 文件:

                MyClass.hpp

                class MyClass
                {
                   #include <MyClass_Part1.hpp>
                   #include <MyClass_Part2.hpp>
                   #include <MyClass_Part3.hpp>
                };
                

                用户可以理论上告诉 GUI 工具每个输入 hpp 文件在哪里以及在哪里创建输出 hpp 文件。

                当然,开发人员可以理论上对 GUI 工具进行编程,以处理任意数量的 hpp 文件(不一定只有 3 个),其前缀可以是任意字符串(不一定只有“MyClass” )。

                别忘了#include &lt;MyClass.hpp&gt; 在您的项目中使用“MyClass”类。

                【讨论】:

                  【解决方案14】:

                  正如所写,这是不可能的,在某些情况下它实际上很烦人。

                  ISO 有一个官方提案,考虑到嵌入式软件,特别是为了避免继承和 pimpl 模式带来的 RAM 开销(这两种方法都需要为每个对象附加一个指针):

                  http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0309r0.pdf

                  很遗憾,提案被拒绝了。

                  【讨论】:

                    【解决方案15】:

                    两次声明类体可能会产生类型重定义错误。如果您正在寻找解决方法。我建议#ifdef'ing,或使用Abstract Base Class 来隐藏特定于平台的详细信息。

                    【讨论】:

                      【解决方案16】:

                      您可以使用模板特化部分特化获得类似部分类的东西。在您投入太多时间之前,请检查您的编译器对这些的支持。像 MSC++ 6.0 这样的旧编译器不支持部分特化。

                      【讨论】:

                        【解决方案17】:

                        这在 C++ 中是不可能的,它会给你一个关于重新定义已经定义的错误 类。如果您想共享行为,请考虑继承。

                        我同意这一点。部分类是一种奇怪的结构,之后很难维护。很难找到每个成员声明在哪个部分类上,并且很难避免重新定义甚至重新实现特性。

                        你想扩展std::vector,你必须继承它。这是因为几个原因。首先,您更改了类的责任和(正确地?)它的类不变量。其次,从安全的角度来看,应该避免这种情况。 考虑一个处理用户身份验证的类......

                        partial class UserAuthentication {
                          private string user;
                          private string password;
                          public bool signon(string usr, string pwd);
                        }
                        
                        partial class UserAuthentication {
                          private string getPassword() { return password; }
                        }
                        

                        还有很多其他的原因……

                        【讨论】:

                        • 它们在 C++ 中并不能真正工作,因为头文件是一个东西,但是在 C# 中,即使一个类是 partial,你也不能在程序集之外添加到它(编译项目)它在里面。如果你想防止继承(在 C# 中),只需创建类sealed;在 C++11 和 final 之前,这是不可能的。
                        【解决方案18】:

                        让平台无关和平台相关的类/函数成为彼此的朋友类/函数。 :)

                        并且它们单独的名称标识符允许对实例化进行更好的控制,因此耦合更松散。部分完全破坏了 OO 的封装基础,而必要的友元声明几乎没有放松它,足以促进多范式分离关注点,例如平台特定方面与领域特定平台无关的方面。

                        【讨论】:

                          【解决方案19】:

                          我一直在我的渲染引擎中做类似的事情。我有一个模板化的 IResource 接口类,从中继承了各种资源(为简洁起见):

                          template <typename TResource, typename TParams, typename TKey>
                          class IResource
                          {
                          public:
                              virtual TKey GetKey() const = 0;
                          protected:
                              static shared_ptr<TResource> Create(const TParams& params)
                              {
                                  return ResourceManager::GetInstance().Load(params);
                              }
                              virtual Status Initialize(const TParams& params, const TKey key, shared_ptr<Viewer> pViewer) = 0;
                          };
                          

                          Create 静态函数回调一个模板化的 ResourceManager 类,该类负责加载、卸载和存储它使用唯一键管理的资源类型的实例,确保从存储中简单地检索重复调用,而不是重新加载为单独的资源。

                          template <typename TResource, typename TParams, typename TKey>
                          class TResourceManager
                          {
                              sptr<TResource> Load(const TParams& params) { ... }
                          };
                          

                          具体资源类利用 CRTP 从 IResource 继承。专门针对每种资源类型的 ResourceManager 被声明为这些类的朋友,因此 ResourceManager 的 Load 函数可以调用具体资源的 Initialize 函数。一个这样的资源是纹理类,它进一步使用 pImpl 习惯用法来隐藏其私有:

                          class Texture2D : public IResource<Texture2D , Params::Texture2D , Key::Texture2D >
                          {
                              typedef TResourceManager<Texture2D , Params::Texture2D , Key::Texture2D > ResourceManager;
                              friend class ResourceManager;
                          
                          public:
                              virtual Key::Texture2D GetKey() const override final;
                              void GetWidth() const;
                          private:
                              virtual Status Initialize(const Params::Texture2D & params, const Key::Texture2D key, shared_ptr<Texture2D > pTexture) override final;
                          
                              struct Impl;
                              unique_ptr<Impl> m;
                          };
                          

                          我们的纹理类的大部分实现是独立于平台的(例如 GetWidth 函数,如果它只返回一个存储在 Impl 中的 int)。但是,根据我们所针对的图形 API(例如 Direct3D11 与 OpenGL 4.3),某些实现细节可能会有所不同。一种解决方案可能是从 IResource 继承一个中间 Texture2D 类,该类定义所有纹理的扩展公共接口,然后从中继承一个 D3DTexture2D 和 OGLTexture2D 类。此解决方案的第一个问题是它要求您的 API 用户不断注意他们所针对的图形 API(他们可以在两个子类上调用 Create)。这可以通过将Create 限制为中间Texture2D 类来解决,该类可能使用#ifdef 开关来创建D3D 或OGL 子对象。但是这个解决方案仍然存在第二个问题,即平台无关的代码将在两个孩子之间重复,从而导致额外的维护工作。您可以尝试通过将独立于平台的代码移动到中间类来解决这个问题,但是如果某些成员数据同时被特定于平台的代码和独立于平台的代码使用,会发生什么情况呢? D3D/OGL 子级将无法访问中介 Impl 中的这些数据成员,因此您必须将它们移出 Impl 并移入标头,以及它们携带的任何依赖项,从而暴露任何包含您的标头的人所有他们不需要知道的废话。

                          API 应该易于正确使用而难以错误使用。易于使用的部分权利是限制用户只接触他们应该使用的 API 部分。该解决方案使其容易被错误使用并增加了维护开销。用户应该只关心他们在一个地方定位的图形 API,而不是他们使用您的 API 的任何地方,并且他们不应该暴露于您的内部依赖项。这种情况需要部分类,但它们在 C++ 中不可用。因此,您可以简单地在单独的头文件中定义 Impl 结构,一个用于 D3D,一个用于 OGL,并在 Texture2D.cpp 文件的顶部放置一个#ifdef 开关,然后通用定义公共接口的其余部分.这样,公共接口可以访问它需要的私有数据,唯一重复的代码是数据成员声明(构造仍然可以在创建 Impl 的 Texture2D 构造函数中完成),您的私有依赖项保持私有,而用户不需要除了在暴露的 API 表面中使用有限的调用集之外,必须关心任何事情:

                          // D3DTexture2DImpl.h
                          #include "Texture2D.h"
                          struct Texture2D::Impl
                          {
                              /* insert D3D-specific stuff here */
                          };
                          
                          // OGLTexture2DImpl.h
                          #include "Texture2D.h"
                          struct  Texture2D::Impl
                          {
                              /* insert OGL-specific stuff here */
                          };
                          
                          // Texture2D.cpp
                          #include "Texture2D.h"
                          
                          #ifdef USING_D3D
                          #include "D3DTexture2DImpl.h"
                          #else
                          #include "OGLTexture2DImpl.h"
                          #endif
                          
                          Key::Texture2D Texture2D::GetKey() const
                          {
                              return m->key;
                          }
                          // etc...
                          

                          【讨论】:

                            猜你喜欢
                            • 1970-01-01
                            • 1970-01-01
                            • 2012-10-28
                            • 1970-01-01
                            • 2011-07-04
                            • 1970-01-01
                            • 2021-04-16
                            • 1970-01-01
                            • 1970-01-01
                            相关资源
                            最近更新 更多