Czy istnieje w C++ odpowiednik pascalowego „class of”?

1

W Pascalu istnieje konstrukcja class of, która umożliwia deklarację typu danych, służącego do przechowywania referencji klasy (klasy, nie instancji klasy zwanej obiektem). Taki typ danych może być później użyty w jakichś fabrykach, np. w parametrze metody wypluwającej obiekty — podaje się w parametrze klasę, ona zostaje użyta do stworzenia instancji (obiektu) i zwrócenia referencji.

Zróbmy przykład. Mamy bazową klasę przedmiotu — ma ona wspólne dane, tutaj nazwę:

type
  TCustomItem = class(TObject)
  public
    Name: String;
  end;

Teraz zadeklarujmy kilka klas konkretnych przedmiotów, dziedziczących z bazowej klasy:

type
  TOrange = class(TCustomItem);
  TBanana = class(TCustomItem);

Wszystkie te klasy dziedziczą pole Name, więc każda posiada swoją nazwę. Aby móc wygodnie tworzyć instancje tych owoców, przyda się metoda (jedna), która będzie pobierać w parametrze referencję klasy i nazwę, będzie tworzyć instancję, uzupełniać wspólne dane i finalnie zwracać obiekt. Najpierw potrzebujemy typu danych, dzięki któremu możliwe będzie przekazanie referencji klasy:

type
  TCustomItemClass = class of TCustomItem;

Zmienna tego typu może przyjąć referencję dowolnej klasy, zarówno TCustomItem, jak i każdej z niej dziedziczącej. Ostatnim krokiem jest napisanie kodu funkcji tworzącej i wypluwającej obiekty owoców:

function CreateItem(AClass: TCustomItemClass; const AName: String): TCustomItem;
begin
  Result := AClass.Create();
  Result.Name := AName;
end;

Jak widać, wewnątrz tej funkcji nie wiemy jaką klasę poda użytkownik. Do stworzenia instancji używamy klasy podanej w parametrze AClass i inicjalizujemy tylko te pola, które istnieją w klasie bazowej. I teraz przykład użycia:

var
  Items: TItems;
  Item: TCustomItem;
begin
  Items := TItems.Create();
  try
    Items.Add(CreateItem(TOrange, 'orange'));
    Items.Add(CreateItem(TBanana, 'banana'));
    Items.Add(CreateItem(TCustomItem, 'custom'));

    for Item in Items do
      WriteLn(Item.Name);
  finally
    Items.Free();
  end;
end.

Jak widać funkcja CreateItem tworzy instancje zadanych klas i referencje obiektów lądują w liście. Na wyjściu dostaniemy:

orange
banana
custom

Pełny kod testowy znajdziecie tutaj — https://ideone.com/b2Fa2N

Skoro wiemy już jak to działa, teraz pytanie — czy w C++ jest odpowiednik pascalowego class of, czyli konstrukcja pozwalająca zadeklarować typ danych do przechowywania referencji klasy? Kolega pyta jakby co. ;)

6

Funkcja tworząca obiekty klasy może po prostu być szablonem inicjowanym odpowiednim typem:

#include <string>
#include <iostream>
#include <memory>
#include <vector>

class TCustomItem {
public:
    std::string name;
    virtual ~TCustomItem() = default;
};

class Orange: public TCustomItem {
};


class Banana: public TCustomItem {
};

template<typename T>
std::unique_ptr<T> create_item(std::string name) {
    auto ptr = std::make_unique<T>();
    ptr->name = name;
    return ptr;
}

#include <typeinfo>
int main() {
    std::vector<std::unique_ptr<TCustomItem>> items;

    items.push_back(create_item<TCustomItem>("Custom"));
    items.push_back(create_item<Orange>("Orange"));
    items.push_back(create_item<Banana>("Banana"));

    for(auto& item: items) {
        std::cout << item->name << std::endl;
    }

}
0

Hmm… ciekawe rozwiązanie, ale nie wiem dokładnie czym są i do czego służą szablony w C++, więc najpierw będę się musiał w tym temacie dokształcić. Choć jeśli mamy działać na generykach, to i w Pascalu mogę ich użyć zamiast class of:

generic function CreateItem<T: class>(const AName: String): TCustomItem;
begin
  Result := T.Create();
  Result.Name := AName;
end;

I podobnie jak w Twoim przykładzie, składnia wywołania CreateItem będzie taka sama:

var
  Items: TItems;
  Item: TCustomItem;
begin
  Items := TItems.Create();
  try
    Items.Add(specialize CreateItem<TOrange>('orange'));
    Items.Add(specialize CreateItem<TBanana>('banana'));
    Items.Add(specialize CreateItem<TCustomItem>('custom'));

    for Item in Items do
      WriteLn(Item.Name);
  finally
    Items.Free();
  end;
end.

W trybie {$MODE DELPHI} nie trzeba podawać specialize, ale w OBJFPC trzeba (tak gdyby się ktoś zastanawiał). Nie podam linku do ideone, bo oni mają FPC w wersji 3.0.4, a funkcje generyczne są w wersji 3.2.0, ale mniejsza z tym. :D

@Spearhead: napisałeś, że można to załatwić szablonem — czyli jest inna opcja? Przypomnę jeszcze pierwotne pytanie — czy da się taki typ przeportować 1:1 z Pascala, czy po prostu C++ nie posiada takiej konstrukcji, bo rządzi się innymi prawami i realizuje się to inaczej?

0

Jakbym musiał tak wydziwiać, to pewnie robiłbym to w Pythonie. Tam by to wyglądało czytelniej - przynajmniej w takich krótkich przykładach :]

0

@Spine: tu nie chodzi o ten konkretny kod, bo to tylko przykład pokazujący o co chodzi z tym class of. :P

2

Może być też bardziej prymitywne niż podał @Spearhead

#include <string>
#include <iostream>
#include <map>
using namespace std;

class TCustomItem 
{
	public:
	string name;
};

class Orange: public TCustomItem {};
class Banana: public TCustomItem {};

enum CustomTypes:int { clCustom,clOrange,clBanana };
TCustomItem *create_item(CustomTypes type,const string &name)
{
	typedef TCustomItem *CustomItemMaker();
	const static map<CustomTypes,CustomItemMaker*> makers=
	{
		{clCustom,[](){ return new TCustomItem(); }},
		{clOrange,[](){ return (TCustomItem*)new Orange(); }},
		{clBanana,[](){ return (TCustomItem*)new Banana(); }},
	};
    TCustomItem *ptr=makers.at(type)();
    ptr->name=name;
    return ptr;
}
5

Do porównania typów możesz użyć "typeid". Użycie:

	template<typename T>
	void set_as(const T&value) {
		if ((typeid(T) == type()) && (xnode_setter<T, xnode_storage_meta<T>::storage_type>::supports_copy())) {
			vtable_->copy_(value_, static_cast<const void *>(&value));
		}

Do wyświetlenia identyfikacji typu: "typeid(MyType).name()", gdzie MyType to typ z szablonu. Użycie:

	template<typename DestType>
	static void throwWrongCast(int typeCode) {
		throw std::runtime_error(std::string("Conversion failed, source type: ") + to_string(typeCode) + ", target: " + typeid(DestType).name());
	}

Do powiązania typu z intem możesz użyć własnej struktury:

template<typename T>
struct my_type_code {
	enum { value = 0 };
};

struct my_type_code<bool> {
	enum { value = 1 };
};

użycie:

	static bool cast_to_value(ValueType &output, void **storage, int srcTypeCode) {
        if (srcTypeCode == my_type_code<long double>::value) {
                output = to_string(
                    xnode_get_scalar<long double>(storage));
                return true;
        }
        else {
            return xnode_caster<ValueType, CastPolicy::base_policy>::cast_to_value(output, storage, srcTypeCode);
        }
	}

lub w skrócie: my_type_code<T>::value gdzie T to typ inicjujący szablonu (czy jak to tam się nazywa).

Sorry za rozwlekłe przykłady, ale nie mam za dużo czasu.

0

Wiem, że to nie było pytanie oryginalne @furious programming :), ale jaka jest przewaga rozwiązania z class of nad szablonami? Rozumiem, że to moment (w czasie działania vs w czasie kompilacji), ale czy nie można tego załatwić wirtualnymi metodami (jesli potrzebujemy naprawdę run-time)?

4

@furious programming: przy czym sporo projektów jest kompilowanych z wyłączonym RTTI (runtime-type-info) i konstrucja podana przez @vpiotr wtedy nie zadziała. Nie jest to jakaś ogólna zasada, ale widzę taki trend.
Co w zasadzie chcesz uzyskać i dlaczego takie cudo w runtimie musisz mieć?
EDIT: i jakie wersje C++/kompilatory bierzesz pod uwagę?
EDIT: po przeczytaniu posta Kolegi niżej: to nie chodzi o używanie specyficznych dla architektury/kompilatora rozwązań, bardziej czy masz dostępny standard C++11 i dalsze; to w kontekście templatek może dość sporo ułatwić jeśli jednak compile-time'owe rozwiązanie okazałoby się wystarczać.

0

@alagner: Jeśli brniemy w pisanie pod kompilator, to możemy zrobić sporo rzeczy wychodzących poza standard.

0

Hm, czytam początkowy post jeszcze raz. Te Items.Add wygląda mi na case dla type erasure.
@furious programming obczaj ten talk:

i tę libkę
https://www.boost.org/doc/libs/1_61_0/doc/html/any.html

2

Alternatywą dla szablonów jest użycie std:variant ( C++17 ).

#include <iostream>
#include <vector>
#include <variant>

using namespace std;

struct Fruit { string name; };
struct Orange : Fruit {};
struct Banana : Fruit {};

int main()
{
    vector<variant<Fruit,Orange,Banana>> items;

    items.emplace_back(Fruit{"Custom"});
    items.emplace_back(Orange{"Orange"});
    items.emplace_back(Banana{"Banana"});

    for( const auto& item : items )
    {
         visit( []( const auto& i ){ cout << i.name << endl; } , item );
    }
}

Zobacz także tutaj

6

To, czy ktoś trzyma wektor wskaźników na klasę bazową (jak u mnie w pierwszym poście), czy wektor std::any, jak sugeruje @alagner, czy wektor std::variant jak sugeruje @TomaszLiMoon jest bez znaczenia i w ogóle nie trafia w sendu problemu zadane w pytaniu. A sednem tym jest to, że nie można typu przekazać do funkcji inaczej niż jako parametru szablonu. Można przekazać jedynie obiekty. To nie tak jak w Pythonie, gdzie klasa obiektu sama w sobie jest obiektem innego typu, którą można odczytać, trzymać w zmiennej i przekazywać. W rezultacie funkcja, która ma tworzyć różne typy w zależności ad parametru (a nie parametru szablonu) zawsze będzie zawierała albo jakieś mapowanie (jak podał @_13th_Dragon) albo jakieś drabinki ifów, pokroju

class BaseFoo {
    virtual ~BaseFoo() = default;
};
class Foo1 : public BaseFoo {};
class Foo2 : public BaseFoo {};
class Foo3 : public BaseFoo {};

template <typename... Ts>
std::unique_ptr<BaseFoo>
makeFoo(Ts&&... params) {
    std::unique_ptr<BaseFoo> p;
    if( /* foo1 */) {
        p.reset(new Foo1(std::forward<Ts>(params)...));
    } else if( /* foo2 */ ) {
        p.reset(new Foo2(std::forward<Ts>(params)...));
    } else if( /* foo3 */) {
        p.reset(new Foo3(std::forward<Ts>(params)...));
    }
    return p;
}

std::unique_ptr<BaseFoo> = makeFoo(arguments);

Nie ma, o ile mi wiadomo, konstrukcji typu "utwórz dynamicznie obiekt typu przekazanego jako argument".

0
koszalek-opalek napisał(a):

[…] ale jaka jest przewaga rozwiązania z class of nad szablonami?

Nie powiem wam jaka konkretnie jest przewaga, bo nie programuję w nowoczesnym C++. W Pascalu konstrukcja class of pozwala zadeklarować typ służący do przechowywania typu (tu: klasy). Nie ma tu żadnego rzutowania i innych kombinacji, zmienna po prostu trzyma informacje o typie danych. To czy wykorzystanie takiego typu jest poprawne, określa kompilator w trakcie kompilacji i jeśli coś nie gra, przerywa kompilację.

Takiego typu można użyć do dowolnego celu, zadeklarować normalną zmienną, można też wykorzystać go do deklaracji parametru. To akurat jest obojętne — chodzi w tym głównie o to, aby móc tworzyć instancje klas końcowych, wykorzystując jeden mechanizm (tu: funkcję) operujący na funkcjonalności klasy bazowej.


alagner napisał(a):

Co w zasadzie chcesz uzyskać i dlaczego takie cudo w runtimie musisz mieć?

Nic — kolega pytał czy takie coś istnieje w C++, ale nie umiał zapytać, więc sam to zrobiłem, bom ciekaw. :D


alagner napisał(a):

Hm, czytam początkowy post jeszcze raz. Te Items.Add wygląda mi na case dla type erasure.

W pierwszym poście podałem przykład co można zrobić z typem klasowym — zadeklarowałem klasę bazową i kilka dziedziczących, potem funkcję wypluwającą referencje, te referencje wrzuciłem do listy, aby w pętli wyświetlić ich nazwy, wszystko na potrzeby objaśnienia w czym rzecz. Sednem jest dowiedzenie się czy jest odpowiednik takiej konstrukcji, a nie jak dobrze pisać kod w C++ — nie programuję w tym języku, więc… ale dzięki za wideo. :P


Spearhead napisał(a):

A sednem tym jest to, że nie można typu przekazać do funkcji inaczej niż jako parametru szablonu. Można przekazać jedynie obiekty.

I to jest myślę odpowiedź na moje pytanie — nie da się przekazać klasy, można jedynie obiekty.

Przy czym trochę dziwię się, że dość dużo kodu trzeba w C++ napisać, żeby odwzorować relatywnie prosty pascalowy mechanizm. Do tej pory byłem raczej zdania, że to Pascal jest rozwleklejszy składniowo. Nieważne, nowoczesny C++ jest na tyle rozbudowany i bogaty, że jest w czym wybierać, a że te języki wiele różni, to nie ma sensu próbować portować kodu 1:1, jeśli zajdzie taka potrzeba. Co język to inne techniki i standardy.

No nic, problem uważam za rozwiązany, dziękuję wszystkim za odpowiedzi. ;)

0
furious programming napisał(a):

Przy czym trochę dziwię się, że dość dużo kodu trzeba w C++ napisać, żeby odwzorować relatywnie prosty pascalowy mechanizm. Do tej pory byłem raczej zdania, że to Pascal jest rozwleklejszy składniowo. Nieważne, nowoczesny C++ jest na tyle rozbudowany i bogaty, że jest w czym wybierać, a że te języki wiele różni, to nie ma sensu próbować portować kodu 1:1, jeśli zajdzie taka potrzeba. Co język to inne techniki i standardy.

A może potrzeba zastosowania takiego mechanizmu to błąd projektowy i lepiej się tego wystrzegać, tak jak goto ;) ?
Niepotrzebnie zaciemnia kod. Niby w Pascalu mało się pisze, żeby coś takiego osiągnąć, ale potem może się trudniej debuguje?

Pythonowa wersja :D

class A:
	def f(self):
		print ("Foo")
		
c = A
b = c()
b.f()
4

Nie znam Pascal, ale wygląda mi na to, że class of reprezentuje informację o klasie pochodnej do danej klasy. Jest to jakaś forma meta danych (refleksji).

W C++ nadal nie ma meta danych.
Może w C++23 będą metakalsy, które rozwiążą problem metadanych z poziomu bibliotecznego, a nie z poziomu składni języka, (miało być w C++20, ale się nie załapało i nadal nie jest pewne czy będzie w C++23).

Czy w obecnie dostępnych standardach C++ da się uzyskać funkcjonalność zbliżoną do class of?
Jedną z możliwości rozwiązania tego, jest napisanie biblioteki, która potrafiłaby ogarnąć tworzenie obiektów wyznaczonego typu, pochodnego do jakiejś klasy bazowej.
Moim zdaniem jest to jak najbardziej wykonalne za pomocą szablonów. Pytanie jedynie jakie funkcjonalności typu reprezentowanego przez class of są potrzebne.
Nie dziwiłbym się jeśli jest już to gdzieś zrobione (nie znalazłem nic googlem).

Zacząłem sam dłubać, ale na zbyt ambitnym poziomie abstrakcji, więc zajmie to więcej czasu (może spróbuję w weekend, teraz trzeba zając się pracą ;)).
EDIT: zalążek pokazujący pomysł: https://godbolt.org/z/odh7an719

0

Nie znam Pascala, ale to trochę przypomina TSubclassOf<T> z UE4.

5

Różnica jest już w zasadach działania konstruktorach Delphi/C++.
W Delphi konstruktor jest traktowany jako metoda więc może być wirtualny i jest jak każda inna metoda dziedziczony przez klasy pochodne.
Natomiast w C++ konstruktor nie może być wirtualny owszem jest dziedziczony przez klasy pochodne ale ponieważ nie jest to metoda wirtualna to jest to zupełnie inna metoda.
Z powyższego wynika że takiego typu trzymającego typ klasy w C++ nie ma i być nie może.
Aby stworzyć odpowiednik trzeba stworzyć wirtualną metodę np Create (może być z parametrami) która będzie tworzyć obiekt danej klasy i zwracać wskaźnik/*_ptr do klasy bazowej.
Wtedy dopiero to będzie odpowiednikiem konstruktora w Delphi, z tym że tak czy owak trzeba jakoś bazować się na type obiektu np typeid.

#include <typeinfo>
#include <string>
#include <iostream>
#include <memory>
#include <map>
using namespace std;

class TCustomItem
{
	public:
	string name;
	virtual ~TCustomItem() = default;
	virtual void who() { cout<<"CustomItem"<<endl; }
};

class Orange: public TCustomItem { public: virtual void who() { cout<<"Orange"<<endl; } };
class Banana: public TCustomItem { public: virtual void who() { cout<<"Banana"<<endl; } };

unique_ptr<TCustomItem> create_item(const type_info &kind,const string &name)
{
	typedef void makerProc(unique_ptr<TCustomItem> &ptr);
	static const map<size_t,makerProc*> dictionary= // dla starszych c++ można użyć string/typeid(**).name()
	{
		{typeid(TCustomItem).hash_code(),[](unique_ptr<TCustomItem> &ptr){ ptr.reset(new TCustomItem()); }},
	 	{typeid(Orange).hash_code(),[](unique_ptr<TCustomItem> &ptr){ ptr.reset(new Orange()); }},
		{typeid(Banana).hash_code(),[](unique_ptr<TCustomItem> &ptr){ ptr.reset(new Banana()); }},
	};
	unique_ptr<TCustomItem> ptr;
	dictionary.at(kind.hash_code())(ptr);
	ptr->name=name;
	return ptr;
}

int main()
{
	const type_info &kind=typeid(Orange);
	unique_ptr<TCustomItem> ptr=create_item(kind,"orange");
	ptr->who();
	return 0;
}
0
MarekR22 napisał(a):

Nie znam Pascal, ale wygląda mi na to, że class of reprezentuje informację o klasie pochodnej do danej klasy. Jest to jakaś forma meta danych (refleksji).

Do takiego typu danych łapią się wszystkie klasy, od bazowej poprzez wszystkie pochodne. I tak, class of reprezentuje informacje o klasie, na podstawie której można tworzyć instancje lub uzyskiwać dostęp do ich danych, bez znajomości konkretnego typu, jaki jest w nich przechowywany.

Czyli w skrócie — zmienna/parametr przechowuje jakąś klasę, nie wiemy jaką, możemy tworzyć jej instancje, wywoływać metody i modyfikować dane (te, które istnieją w klasie bazowej), w pełni respektując dziedziczenie (jeśli klasa pochodna nadpisuje jakąś metodę wirtualną, to taka zostanie wywołana, a nie jej prototyp bazowy).

2

Chyba nic lepszego niż to co napisał Smoku się nie da zrobić, bo:

  • Pascal ma rozbudowane RTTI i właśnie ma dostęp do konstruktorów niewiadomego pochodzenia (to jest jego jedna z większych zalet)
  • C++ ma minimalne RTTI a szablony nie są runtime tylko compile-time, dlatego to co jest w Pascalu trzeba emulować swoimi konstruktami
  • w C++ najprościej użyć po prostu fabryki: https://sourcemaking.com/design_patterns/factory_method/cpp/1
1
MarekR22 napisał(a):

Zacząłem sam dłubać, ale na zbyt ambitnym poziomie abstrakcji, więc zajmie to więcej czasu (może spróbuję w weekend, teraz trzeba zając się pracą ;)).

https://github.com/MarekR22/class_of_in_cpp/blob/main/src/pofa/pofa.hpp
https://godbolt.org/z/K65rYx4cc

#ifndef POLY_FACTORY_HEADER
#define POLY_FACTORY_HEADER

#include <memory>
#include <type_traits>

namespace pofa {

namespace detail {
template<typename Base>
using safe_unique_ptr = std::conditional_t<
	std::has_virtual_destructor_v<Base>,
		std::unique_ptr<Base>,
		std::unique_ptr<Base, void (*)(Base *)>
	>;

template<typename Base, typename CtorOverloads>
class ctor_interface;

template<typename Base, typename ...Args>
class ctor_interface<Base, void(Args...)>{
public:
    virtual ~ctor_interface() {}
    
    virtual auto create_unique(Args...) const -> safe_unique_ptr<Base> = 0;
    virtual auto create_shared(Args...) const -> std::shared_ptr<Base> = 0;
};
}

template<typename Base, typename...CtorOverload>
class creator_of : public detail::ctor_interface<Base, CtorOverload>...
{
public:
    template<typename Child>
    static auto instance() -> const creator_of*;
    
    using detail::ctor_interface<Base, CtorOverload>::create_unique ...;
    using detail::ctor_interface<Base, CtorOverload>::create_shared ...;
};

namespace detail {
template <typename Base, typename Child, typename Interface, typename...CtorOverload>
class creator_of_instance;

template <typename Base, typename Child, typename Interface>
class creator_of_instance<Base, Child, Interface> : public Interface
{};


template <typename Base, typename Child, typename Interface, typename...FirstCtorArgs, typename...CtorOverload>
class creator_of_instance<Base, Child, Interface, void(FirstCtorArgs...), CtorOverload...>
	: public creator_of_instance<Base, Child, Interface, CtorOverload...>
{
public:
    auto create_unique(FirstCtorArgs...args) const -> safe_unique_ptr<Base> override
    {
        if constexpr (std::is_constructible_v<Child, FirstCtorArgs...>)
        {
        	return std::make_unique<Child>(std::forward<FirstCtorArgs>(args)...);
        }
        throw std::logic_error{""};
    }
    
    auto create_shared(FirstCtorArgs...args) const -> std::shared_ptr<Base> override
    {
        if constexpr (std::is_constructible_v<Child, FirstCtorArgs...>)
        {
        	return std::make_shared<Child>(std::forward<FirstCtorArgs>(args)...);
        }
        throw std::logic_error{""};
    }
};
}

template<typename Base, typename...CtorOverload>
template<typename Child>
auto creator_of<Base, CtorOverload...>::instance() -> const creator_of*
{
    using Interface = creator_of<Base, CtorOverload...>;
    
    static const detail::creator_of_instance<Base, Child, Interface, CtorOverload...> creator;
    return &creator;
}
}

#endif // POLY_FACTORY_HEADER

Nadal in progress, jeszcze parę rzeczy udoskonalę (po to jest np safe_unique_ptr).

1 użytkowników online, w tym zalogowanych: 0, gości: 1