Подтвердить что ты не робот

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

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

Что-то вроде:

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(), но есть другие способы избежать этого.

4b9b3361

Ответ 1

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

Ответ 2

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

В частности

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

а затем

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

Ответ 3

Вы не можете частично определять классы в С++.

Здесь вы можете получить "полиморфизм, где есть только один подкласс", который вам нужен без накладных расходов и с минимальным размером #define или дублирования кода. Он назывался симулированной динамической привязкой:

template <typename T>
class genericTest {
public:
    void genericMethod() {
        // do some generic things
        std::cout << "Could be any platform, I dunno" << 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), но если встроенный конструктор con (de) для genericTest является строгим, вы должны быть в порядке и что накладные расходы ни в коем случае не хуже, чем метод genericInit, вызываемый обе платформы.

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

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

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

Ответ 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

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

Кроме того, разработчики делали это в течение десятилетий без этой "функции".

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

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

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

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

Грязный, но практичный способ использует препроцессор #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

Ответ 10

Неа.

Но вы можете захотеть найти технику под названием "Классы политики". В принципе, вы делаете микро-классы (которые не полезны сами по себе), а затем склеивайте их вместе в какой-то более поздний момент.

Ответ 11

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

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

Ответ 12

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

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

Вы хотите расширить 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; }
}

Можно упомянуть много других причин...

Ответ 13

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

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

Ответ 14

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

Ответ 15

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

Ответ 16

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

Ответ 17

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

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

Ответ 18

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

Ответ 19

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

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" в ваших проектах.