Определение частичного класса на C++?

Кто-нибудь знает, возможно ли определение частичного класса на С ++?

Что-то типа:

file1.h:

  
class Test {
    public:
        int test1();
};

file2.h:

class Test {
    public:
        int test2();
};

Для меня это кажется весьма полезным для определения многоплатформенных классов, у которых есть общие функции между ними, которые не зависят от платформы, потому что наследование - это плата, которая бесполезна для многоплатформенных классов.

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

Также вам придется использовать уродливый #ifdef для использования класса, потому что вы не можете создать экземпляр из абстрактного класса:

class genericTest {
    public:
        int genericMethod();
};

Тогда скажем для win32:

class win32Test: public genericTest {
    public:
        int win32Method();
};

И возможно:

class macTest: public genericTest {
    public:
        int macMethod();
};

Предположим, что и win32Method (), и macMethod () вызывают genericMethod (), и вам придется использовать этот класс следующим образом:

 #ifdef _WIN32
 genericTest *test = new win32Test();
 #elif MAC
 genericTest *test = new macTest();
 #endif

 test->genericMethod();

Теперь подумайте, что наследование было полезно только для того, чтобы дать им обоим genericMethod (), который зависит от платформы, зависящей от платформы, но из-за этого у вас есть затраты на вызов двух конструкторов. Также у вас есть уродливый #ifdef, разбросанный по коду.

Вот почему я искал частичные классы. Я мог бы во время компиляции определить частичный конец, зависящий от конкретной платформы, конечно, что в этом глупом примере мне все еще нужен уродливый #ifdef внутри genericMethod (), но есть и другие способы избежать этого.

Во-первых, невиртуальные функции не несут «затрат», а во-вторых, если ваш код не указывает, какую версию интерфейса вы используете, работа над одной версией нарушит работу другой версии, например когда вы выполняете test-> win32Method (). Классы, специфичные для платформы, не совпадают и заслуживают разных имен.

Jamie 29.09.2008 02:03

И вы можете использовать фабричный метод, чтобы минимизировать #ifdefs. И вы можете использовать шаблоны (en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern), чтобы еще больше уменьшить его.

Jamie 29.09.2008 02:06

C++ - такой старый и плохой язык ((

user2440074 15.09.2015 10:18

C++ / CX от Microsoft поддерживает частичный класс с нестандартными ключевыми словами, partial и ref, непосредственно перед ключевым словом class, а вот его документ. Я бы предпочел избегать использования этой функции, потому что это не стандартный C++.

KaiserKatze 12.01.2020 04:28
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
37
4
38 288
19
Перейти к ответу Данный вопрос помечен как решенный

Ответы 19

Попробуйте наследование

Конкретно

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

а потом

class PlatformA : public AllPlatforms {
public:
    int specific();
};
Ответ принят как подходящий

Это невозможно в C++, это приведет к ошибке при переопределении уже определенных классов. Если вы хотите поделиться поведением, рассмотрите возможность наследования.

Как написано, это невозможно.

Вы можете изучить пространства имен. Вы можете добавить функцию в пространство имен в другом файле. Проблема с классом в том, что каждый .cpp должен видеть полный макет класса.

Неа.

Но вы можете найти метод, называемый «Классы политики». По сути, вы создаете микроклассы (которые сами по себе бесполезны), а потом склеиваете их вместе.

Довольно круто! В Википедии есть хороший пример: en.wikipedia.org/wiki/Policy-based_design

Andrew 26.09.2016 11:36

Двойное объявление тела класса, скорее всего, вызовет ошибку переопределения типа. Если вы ищете обходной путь. Я бы предложил # ifdef'ing или использовать Абстрактный базовый класс, чтобы скрыть детали платформы.

Либо используйте наследование, как сказал Джейми, либо #ifdef, чтобы разные части компилировались на разных платформах.

Поскольку заголовки вставляются просто текстуально, один из них может опустить "class Test {" и "}" и быть #included в середине другого.

Я действительно видел это в производственном коде, хотя Delphi не C++. Это особенно раздражало меня, потому что это нарушало функции навигации по коду IDE.

Это очень ошибочно, очень ошибочно, но, на мой взгляд, правильное решение :-)

kervin 26.09.2008 22:05

For me it seems quite useful for definining multi-platform classes that have common functions between them that are platform-independent.

Только вот разработчики десятилетиями делали это без этой «функции».

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

Сгенерированный код часто становится кошмаром для обслуживания. Какие привычки у всей этой инфраструктуры, созданной MFC, когда вам нужно поднять версию MFC? Или как перенести весь этот код в файлы * .designer.cs при обновлении Visual Studio?

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

Я никогда не видел, чтобы «частичное» использовалось там, где наследование или файл ресурсов конфигурации не справились бы лучше.

Что делать, если у вас есть класс интерфейса шаблона, от которого вы наследуете CRTP-моду, с которым также разговаривает другой класс шаблона с теми же специализациями, а классы, производные от интерфейса и использующие идиому pImpl, имеют идентичные реализации для всех общедоступных функций, но должны иметь разные реализации, зависящие от платформы, для всех частных функций? Частичные классы здесь будут чище, чем включение отдельных файлов Impl над реализацией общедоступного интерфейса на основе переключателя #ifdef.

orfdorf 12.01.2015 03:57

Очень поздний ответ, но эмулятор с отдельными функциями для каждой формы инструкции будет проще IMO с частичными классами. Если вы поместите все объявления в один файл (который требует C++), у вас будет 1000-е годы определений функций в один файл!. Для Bochs посмотрите на конец cpu/cpu.h: sourceforge.net/p/bochs/code/HEAD/tree/trunk/bochs/cpu/cpu.h. Если бы частичные классы существовали, вы могли бы разделить эти определения. Хотя это и не обязательно, это значительно уменьшит размер файла.

Cole Johnson 03.03.2020 19:25

Вы можете получить что-то вроде частичных классов, используя специализация шаблона и частичная специализация. Прежде чем тратить слишком много времени, проверьте, поддерживает ли это ваш компилятор. Старые компиляторы, такие как MSC++ 6.0, не поддерживали частичную специализацию.

или вы можете попробовать PIMPL

общий файл заголовка:

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

Затем создайте файлы cpp, выполняя пользовательские реализации, зависящие от платформы.

Как насчет этого:

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 - это класс, известный во время компиляции, поэтому никакие накладные расходы не связаны с абстрактными базовыми классами или чем-то еще.

#include will work as that is preprocessor stuff.

class Foo
{
#include "FooFile_Private.h"
}

////////

FooFile_Private.h:

private:
  void DoSg();

Вы не можете частично определять классы в 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) для genericTest встроен, все должно быть в порядке, и эти накладные расходы в любом случае не хуже, чем наличие метода genericInit, вызываемого обе платформы.

Клиентский код просто создает экземпляры Test и может вызывать для них методы, которые либо находятся в genericTest, либо в правильной версии для платформы. Чтобы помочь с безопасностью типов в коде, который не заботится о платформе и не хочет случайно использовать специфичные для платформы вызовы, вы можете дополнительно сделать:

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

При использовании BaseTest нужно быть немного осторожнее, но не более того, чем всегда бывает с базовыми классами в C++. Например, не делайте этого с необдуманной передачей ценности. И не создавайте его напрямую, потому что, если вы это сделаете и вызовете метод, который в конечном итоге попытается выполнить «поддельный виртуальный» вызов, у вас возникнут проблемы. Последнее можно обеспечить, убедившись, что все конструкторы genericTest защищены.

Я думаю, у вас такая же мысль, мне это понравилось.

Edwin Jarvis 30.09.2008 04:05

Вы сможете найти больше советов и уловок, выполнив поиск по «симулированному динамическому связыванию» или, возможно, «статическому полиморфизму». Я не уверен, что когда-либо серьезно использовал его, поскольку на практике накладные расходы на виртуальные вызовы довольно редко действительно имеют значение. Но если это когда-нибудь случится, я буду готов :-)

Steve Jessop 30.09.2008 15:46

This is not possible in C++, it will give you an error about redefining already-defined classes. If you'd like to share behavior, consider inheritance.

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

Вы хотите расширить 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 это было невозможно.

Cole Johnson 03.03.2020 19:28

Пусть независимые от платформы и зависящие от платформы классы / функции будут друг друга дружественными классами / функциями. :)

И их отдельные идентификаторы имен позволяют более точно контролировать создание экземпляров, поэтому связь становится более слабой. Частичное нарушение основы инкапсуляции объектно-ориентированного подхода слишком сильно, в то время как необходимые объявления друзей едва ли ослабляют его, достаточно, чтобы облегчить многопарадигмальное разделение проблем, таких как аспекты, специфичные для платформы, от аспектов, зависящих от конкретной платформы.

Грязный, но практичный способ - использовать препроцессор #include:

Test.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

Я делал нечто подобное в моем движке рендеринга. У меня есть шаблонный интерфейсный класс 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) { ... }
};

Конкретные классы ресурсов наследуются от IResource с использованием CRTP. ResourceManager, специализирующийся на каждом типе ресурса, объявляется друзьями этих классов, так что функция Load ResourceManager может вызывать функцию 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, если она просто возвращает int, хранящийся в Impl). Однако в зависимости от того, какой графический 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, и поместить переключатель #ifdef в верхнюю часть файла Texture2D.cpp, а остальную часть общедоступного интерфейса определить универсально. Таким образом, общедоступный интерфейс имеет доступ к нужным частным данным, единственный повторяющийся код - это объявления членов данных (конструкция все еще может выполняться в конструкторе Texture2D, который создает Impl), ваши частные зависимости остаются частными, а пользователи - нет. нужно заботиться обо всем, кроме использования ограниченного набора вызовов в открытой поверхности 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...

Предположим, что у меня есть:

MyClass_Part1.hpp, MyClass_Part2.hpp и MyClass_Part3.hpp

Теоретически кто-то может разработать инструмент с графическим интерфейсом, который считывает все эти файлы 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 <MyClass.hpp> использовать класс MyClass в ваших проектах.

Как написано, это невозможно, а в некоторых случаях это действительно раздражает.

В ISO поступило официальное предложение с учетом встроенного программного обеспечения, в частности, чтобы избежать чрезмерного увеличения объема оперативной памяти, вызванного как наследованием, так и шаблоном pimpl (оба подхода требуют дополнительного указателя для каждого объекта):

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

К сожалению, предложение было отклонено.

Другие вопросы по теме