constexpr Class Builder

0

Cześć,

próbuję stworzyć class buildera, który rozpakuje mi się do parametrów template'a.

Powiedzmy, że mam taką klasę:

enum class PrinterOption
{
    NotSet = 0,
    ToStdout,
    ToFile,
};

template< PrinterOption V1, int V2, int V3, bool V4, int V5 >
class Printer
{
public:
    static void SomeHotFunction()
    {
        if( V1 == PrinterOption::ToStdout )
        {
            printf( "SomeHotFunction - PrinterOption::ToStdout, V2 = %d, V3 = %d, V4 = %d, V5 = %d\n", V2, V3, V4, V5 );
        }
        else if( V1 == PrinterOption::ToFile )
        {
            printf( "SomeHotFunction - PrinterOption::ToFile, V2 = %d, V3 = %d, V4 = %d, V5 = %d\n", V2, V3, V4, V5 );
        }
        else
        {
            abort();
        }
    }
};

i mam do niej buildera:

class PrinterBuilder
{
public:
    constexpr PrinterBuilder()
    {
    }

    constexpr PrinterBuilder &SetToStdout( int v2, int v3 )
    {
        m_v1 = PrinterOption::ToStdout;
        m_v2 = v2;
        m_v3 = v3;
        return *this;
    }

    constexpr PrinterBuilder &SetToFile( bool v4, int v5 )
    {
        m_v1 = PrinterOption::ToFile;
        m_v4 = v4;
        m_v5 = v5;
        return *this;
    }

    constexpr auto GetTuple() const
    {
        return std::make_tuple( m_v1, m_v2, m_v3, m_v4, m_v5 );
    }

public:
    PrinterOption m_v1 = PrinterOption::NotSet;
    int m_v2 = 1;
    int m_v3 = 4;
    bool m_v4 = false;
    int m_v5 = 10;
};

i przykładowe użycie:

int main()
{
    static constexpr auto p1 = PrinterBuilder().SetToStdout( 100, 200 ).GetTuple();
    using Printer1 = Printer< std::get< 0 >( p1 ), std::get< 1 >( p1 ), std::get< 2 >( p1 ), std::get< 3 >( p1 ), std::get< 4 >( p1 ) >;
    Printer1::SomeHotFunction();

    static constexpr auto p2 = PrinterBuilder().SetToFile( true, 10000 ).GetTuple();
    using Printer2 = Printer< std::get< 0 >( p2 ), std::get< 1 >( p2 ), std::get< 2 >( p2 ), std::get< 3 >( p2 ), std::get< 4 >( p2 ) >;
    Printer2::SomeHotFunction();

    return 0;
}

Otóż potrzebuję generalnego rozwiązania do tego kawałka kodu:

static constexpr auto p2 = PrinterBuilder().SetToFile( true, 10000 ).GetTuple();
using Printer2 = Printer< std::get< 0 >( p2 ), std::get< 1 >( p2 ), std::get< 2 >( p2 ), std::get< 3 >( p2 ), std::get< 4 >( p2 ) >;

konkretnie to potrzebuję jakiejś magii, która sprawi, że parametry z std::tuple rozpakują mi się w miejscu parametrów szablonowych.

Coś takiego:

    static constexpr auto p2 = PrinterBuilder().SetToFile( true, 10000 ).GetTuple();
    using Printer2 = Printer< MAGIA( p2 ) >;

A jakby dało się zrobić jeszcze coś takiego:

    using Printer2 = Printer< MAGIA( PrinterBuilder().SetToFile( true, 10000 ) ) >;

To już w ogóle byłbym wniebowzięty.


Uprzedzając pytania, odpowiadam po co mi to w ogóle jest potrzebne.

Nie mogę przenieść parametrów szablonowych do składowej klasy, powiedzmy:

class Printer
{
    PrinterOption V1;
    int V2;
    int V3;
    bool V4;
    int V5;

Bo wtedy ta funkcja:

static void SomeHotFunction()
{
    if( V1 == PrinterOption::ToStdout )

będzie miała warunek w runtime.

Z parametrami szablonowymi ta funkcja wygląda bardzo prosto w asm:

call    Printer<(PrinterOption)1, 100, 200, false, 10>::SomeHotFunction()
        .string "SomeHotFunction - PrinterOption::ToStdout, V2 = %d, V3 = %d, V4 = %d, V5 = %d\n"
Printer<(PrinterOption)1, 100, 200, false, 10>::SomeHotFunction():
        push    rbp
        mov     rbp, rsp
        mov     r8d, 10
        mov     ecx, 0
        mov     edx, 200
        mov     esi, 100
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        nop
        pop     rbp
        ret

Brak warunków, prosty kod do wykonania.
Parametry mogą być złożone, więc może być wiele warunków do nich i dodatkowo pamięć parametrów może nie być w cache. Wszystkie parametry są znane w czasie kompilacji, jedynie miejsc użycia klasy Printer<> może być bardzo wiele.

Założeniem systemu jest to, że funkcja SomeHotFunction() (która dodatkowo będzie zinlineowana) będzie wołana na serwerze przynajmniej 80000000 razy na sekundę na wielu wątkach przypiętych do rdzeni i nie da się tego uniknąć.

Czy to jest w ogóle możliwe, żeby to zrobić?

Godbolt: https://godbolt.org/z/ccYvrohG6

0

Widzę że nie pojąłeś idei buildera.
Czy klasa Printer to twoja klasa?
Jakie ma konstruktory?

0

Nazwałem to builderem bo to najbliżej jest tej koncepcji.

Ja piszę framework, który będzie używany w całej aplikacji.

Klasa Printer jest częścią frameworka i sterowanie ma być oddane userowi (przez parametry).

0

W języku bez refleksji stworzenie uniwersalnego jest nie do przebrnięcia.
Więc zwyczajnie o ile chcesz mieć builder to wygląda to jakoś tak:

#include <iomanip>
#include <iostream>
#include <vector>
#include <cctype>
#include <cstdlib>
using namespace std;

enum class PrinterOption:uint8_t
{
    NotSet = 0,
    ToStdout,
    ToFile,
};

ostream &operator<<(ostream &s,const PrinterOption &p)
{
	if(p==PrinterOption::ToStdout) s<<"ToStdout";
	else if(p==PrinterOption::ToFile) s<<"ToFile";
	else s<<"NotSet";
	return s;
}

class Printer
{
	public:
	static void call(PrinterOption V1,int V2,int V3,bool V4,int V5) 
	{
		cout<<"------------"<<endl;
		cout<<"V1="<<V1<<';'<<endl;
		cout<<"V2="<<V2<<';'<<endl;
		cout<<"V3="<<V3<<';'<<endl;
		cout<<"V4="<<V4<<';'<<endl;
		cout<<"V5="<<V5<<';'<<endl;
		cout<<endl;
	}
};

class Builder
{
	private:
	PrinterOption V1;
	int V2,V3;
	bool V4;
	int V5;
	public:
	Builder():V1(PrinterOption::NotSet),V2(100),V3(200),V4(false),V5(10) {} //default options
	Builder &setV1(PrinterOption V1) { this->V1=V1; return *this; }
	Builder &setV2(int V2)           { this->V2=V2; return *this; }
	Builder &setV3(int V2)           { this->V3=V3; return *this; }
	Builder &setV4(bool V4)          { this->V4=V4; return *this; }
	Builder &setV5(int V5)           { this->V5=V5; return *this; }
	Printer call()const { return Printer::call(V1,V2,V3,V4,V5); }
};

int main()
{
	Builder builder;
	Printer p1=builder.setV1(PrinterOption::ToStdout).setV5(11).call();
	Printer p2=builder.setV1(PrinterOption::ToFile).setV4(true).call();
	Printer p3=builder.setV2(666).setV3(13).call();
	return 0;
}
0

Ok, trzeba więcej szczegółów.

Buduję framework do replikacji obiektów w grze sieciowej.

Gra obsługuje wiele klientów w jednym czasie (4000) i jest to gra symulacyjna. Do systemu wchodzić może nawet 50000 obiektów, które się składają z różnych właściwości (składowych).

Aby móc sobie pozwolić na tyle połączeń jednocześnie i tyle obiektów do replikacji, trzeba sensownie zarządzać systemem replikacji. Nie wchodząc za bardzo w szczegóły, framework musi mieć kod wynikowy, który zrobi robotę jak najszybciej. Każda najmniejsza rzecz, którą dodamy do Hot funkcji będzie spowalniać cały system o milisekundy, na co sobie nie możemy pozwolić (ramka trwa 16 ms, jak się nie zmieścimy w ramce to bardzo źle).

Aby replikować obiekty z serwera do klienta, najczęściej robi się to refleksją (tzn ma to sens, przetestowane w boju - tak to robią duże silniki gier).

Zbudowanie refleksji to żaden problem. Kod, który serializuje obiekty, robi delta compression, można wygenerować z szablonów i będzie on ładnie poinlineowany, bez virtual calli, dobrze zarządzany.

Powiedzmy, że mamy encję aktora, który ma ilość życia (user code):

// user code
class Actor
{
public:
  int m_health;
};

Refleksję do replikacji dla tego aktora można stworzyć tak:

// user code
REPLICATION_CLASS_BEGIN( Actor )
  REPLICATION_PROPERTY( m_health );
REPLICATION_CLASS_END;

Framework w środku ma obsługę dla różnych typów.

// framework
template< typename T >
class TypeSerializer
{
public:
  static void Serialize( BitStream &bs, T &value );

  // takich funkcji jest więcej, do różnych dziwnych rzeczy związanych z replikacją
};

Obsługa jest realizowana przez specjalizację klasy powyżej.

// framework
template<>
class TypeSerializer< int >
{
public:
  static void Serialize( BitStream &bs, int &value )
  {
     // jakis kod serializujący dane
  }
};

REPLICATION_PROPERTY( m_health ); w zasadzie w pewien sposób tworzy możliwość dla frameworka do tego aby zawołać TypeSerializer< int >::Serialize(). Nie będę pokazywał jak - nie jest to jakaś czarna magia.

Cały problem w tym, że użytkownik doskonale wie jaką charakterystykę ma m_health a framework tego nie wie (bo skąd).
Można by było wtłoczyć do frameworka parametry określające jak framework ma serializować m_health.

REPLICATION_PROPERTY( m_health ).ReplicateWithBitsCount( 5 ); // 5 bitów wystarczy

Pytanie, jak zrobić, żeby wtłoczyć tą wartość do frameworka.

Myślałem, żeby wtłaczać to mniej więcej tak:

// framework
template< typename T, typename Settings >
class TypeSerializer
{
public:
  static void Serialize( BitStream &bs, T &value );

  // takich funkcji jest więcej, do różnych dziwnych rzeczy związanych z replikacją
};

// framework
template< int BitsCount = 32 >
class TypeSerializer_int_Settings
{
public:
  static constexpr int c_bitsCount = BitsCount;
};

class TypeSerializer_int_Settings_Builder
{
public:
  constexpr TypeSerializer_int_Settings_Builder & ReplicateWithBitsCount( int bitsCount )
  {
    m_bitsCount = bitsCount;
    return *this;
  }

  int m_bitsCount;
};

// framework
template< typename Settings = TypeSerializer_int_Settings<> >
class TypeSerializer< int, Settings >
{
public:
  // Implementacja
  static void Serialize( BitStream &bs, int &value )
  {
    bs.WriteWithBits( value, Settings::c_bitsCount /* wtłoczone */ );
  }
};

Nie pytam jak ogarnąć to makro REPLICATION_PROPERTY, bo to ogarnę tak lub inaczej.

Ogólnie, to framework woła TypeSerializer<>::Serialize wtedy kiedy trzeba zreplikować obiekt do peera. Zadaniem usera frameworku jest to aby odpowiednio opisać klasy obieków refleksją i dodać obiekty do replikacji. Resztą zajmuje się framework.

1

Wg mnie szukasz tego co proponuje boost::archive czyli serializacja i deserializacja.
Owszem możesz to skopiować z boosta i przerobić na strumień bitów, ale z autopsji wiem że nie tędy droga.
Nie wiem czy rozumiesz że jak zaczniesz wchodzić w kodowanie bitowe to cały process bardzo się spowolni.

1

Dla potomnych: po kilku dniach wymyśliłem coś co daje mi zadowalający efekt.

enum class PrinterOption
{
    NotSet = 0,
    ToStdout,
    ToFile,
};

template< PrinterOption V1 = PrinterOption::NotSet, int V2 = 200, int V3 = 300, bool V4 = false, int V5 = 400, int V6 = 0 >
class Printer
{
public:
    static void SomeHotFunction()
    {
        if( V1 == PrinterOption::ToStdout )
        {
            printf( "SomeHotFunction - PrinterOption::ToStdout, V2 = %d, V3 = %d, V4 = %d, V5 = %d, V6 = %d\n", V2, V3, V4, V5, V6 );
        }
        else if( V1 == PrinterOption::ToFile )
        {
            printf( "SomeHotFunction - PrinterOption::ToFile, V2 = %d, V3 = %d, V4 = %d, V5 = %d, V6 = %d\n", V2, V3, V4, V5, V6 );
        }
        else
        {
            printf( "SomeHotFunction = PrinterOption::NotSet, V2 = %d, V3 = %d, V4 = %d, V5 = %d, V6 = %d\n", V2, V3, V4, V5, V6 );
        }
    }
};

class PrinterBuilder
{
public:
    template< int V2, int V3 >
    class SetToStdout
    {
    };

    template< bool V4, int V5 >
    class SetToFile
    {
    };

    template< int V6 >
    class SetV6
    {
    };
};

// Implementation of PrinterBuilder::SetToStdout
template< PrinterOption V1, int V2, int V3, bool V4, int V5, int V6, int CV2, int CV3 >
constexpr inline Printer< PrinterOption::ToStdout, CV2, CV3, V4, V5, V6 > operator|( Printer< V1, V2, V3, V4, V5, V6 > p1, PrinterBuilder::SetToStdout< CV2, CV3 > p2 )
{
    return {};
}

// Implementation of PrinterBuilder::SetToFile
template< PrinterOption V1, int V2, int V3, bool V4, int V5, int V6, bool CV4, int CV5 >
constexpr inline Printer< PrinterOption::ToFile, V2, V3, CV4, CV5, V6 > operator|( Printer< V1, V2, V3, V4, V5, V6 > p1, PrinterBuilder::SetToFile< CV4, CV5 > p2 )
{
    return {};
}

// Implementation of PrinterBuilder::SetV6
template< PrinterOption V1, int V2, int V3, bool V4, int V5, int V6, int CV6 >
constexpr inline Printer< V1, V2, V3, V4, V5, CV6 > operator|(Printer< V1, V2, V3, V4, V5, V6 > p1, PrinterBuilder::SetV6< CV6 > p2 )
{
    return {};
}

int main()
{
    using Printer0 = decltype( Printer<>() );
    Printer0::SomeHotFunction();

    using Printer1 = decltype( Printer<>() | PrinterBuilder::SetToStdout< 100, 200 >() | PrinterBuilder::SetV6< 50 >() );
    Printer1::SomeHotFunction();

    using Printer2 = decltype( Printer<>() | PrinterBuilder::SetToFile< true, 10000 >() | PrinterBuilder::SetV6< 150 >() );
    Printer2::SomeHotFunction();

    using Printer3 = decltype( Printer<>() | PrinterBuilder::SetV6< 160 >() | PrinterBuilder::SetToFile< true, 11234 >() );
    Printer3::SomeHotFunction();
}

https://godbolt.org/z/qKfzqzT3E

Ale poszedłem jeszcze dalej i jeszcze bardziej to zgeneralizowałem, żeby móc podawać tylko typy (bez instancjonowania).

template< typename ... Args >
class BuildPrinterImpl {};

template< typename Arg, typename ... Args >
class BuildPrinterImpl< Arg, Args... >
{
public:
    using Result = decltype( std::declval< typename BuildPrinterImpl< Args... >::Result >() | std::declval< Arg >() );
};

template<>
class BuildPrinterImpl<>
{
public:
    using Result = decltype( std::declval< Printer<> >() );
};

template< typename ... Args >
using BuildPrinter = typename BuildPrinterImpl< Args... >::Result;

int main()
{
    using Printer1 = BuildPrinter<
                                   PrinterBuilder::SetToStdout< 100, 200 >,
                                   PrinterBuilder::SetV6< 50 >
                                 >;
    Printer1::SomeHotFunction();

    using Printer2 = BuildPrinter<
                                   PrinterBuilder::SetToFile< true, 10000 >,
                                   PrinterBuilder::SetV6< 150 >
                                 >;
    Printer2::SomeHotFunction();
    
    using Printer3 = BuildPrinter<
                                   PrinterBuilder::SetV6< 160 >,
                                   PrinterBuilder::SetToFile< true, 11234 >
                                 >;
    Printer3::SomeHotFunction();

    using Printer4 = BuildPrinter<
                                   PrinterBuilder::SetToFile< true, 10000 >,
                                   PrinterBuilder::SetV6< 260 >
                                 >;
    Printer4::SomeHotFunction();

    return 0;
}

https://godbolt.org/z/d67rv639b

Ewentualnie BuildPrinter można zgeneralizować żeby przyjmowało jakąkolwiek klasę, nie tylko Printer:

template< typename C, typename ... Args >
class ClassParamsBuilderImpl {};

template< typename C, typename Arg, typename ... Args >
class ClassParamsBuilderImpl< C, Arg, Args... >
{
public:
    using Result = decltype( std::declval< typename ClassParamsBuilderImpl< C, Args... >::Result >() | std::declval< Arg >() );
};

template< typename C >
class ClassParamsBuilderImpl< C >
{
public:
    using Result = decltype( std::declval< C >() );
};

template< typename C, typename ... Args >
using ClassParamsBuilder = typename ClassParamsBuilderImpl< C, Args... >::Result;

int main()
{
    using Printer1 = ClassParamsBuilder< Printer<>,
                                   PrinterBuilder::SetToStdout< 100, 200 >,
                                   PrinterBuilder::SetV6< 50 >
                                 >;
    Printer1::SomeHotFunction();

    using Printer2 = ClassParamsBuilder< Printer<>,
                                   PrinterBuilder::SetToFile< true, 10000 >,
                                   PrinterBuilder::SetV6< 150 >
                                 >;
    Printer2::SomeHotFunction();
    
    using Printer3 = ClassParamsBuilder< Printer<>,
                                   PrinterBuilder::SetV6< 160 >,
                                   PrinterBuilder::SetToFile< true, 11234 >
                                 >;
    Printer3::SomeHotFunction();

    using Printer4 = ClassParamsBuilder< Printer<>,
                                   PrinterBuilder::SetToFile< true, 10000 >,
                                   PrinterBuilder::SetV6< 260 >
                                 >;
    Printer4::SomeHotFunction();

    return 0;
}

Oczywiście parametrami można sterować użyciem takiej liczby setterów jaką chcemy (i kolejność nie ma znaczenia):

    using Printer1 = ClassParamsBuilder< Printer<>,
                                   PrinterBuilder::SetV1< PrinterOption::ToStdout >,
                                   PrinterBuilder::SetV4< false >,
                                   PrinterBuilder::SetV3< 11111 >,
                                   PrinterBuilder::SetV6< 50 >
                                 >;
    Printer1::SomeHotFunction();

Wyświetla:

SomeHotFunction - PrinterOption::ToStdout, V2 = 200, V3 = 11111, V4 = 0, V5 = 400, V6 = 50

https://godbolt.org/z/q41vfrrEP

Działa na gcc, msvc, clang.

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