Jak stworzyć klasę przyjmującą wartość w różnych typach?

0

Cześć, mam problem z poniższym zadaniem.
Treść zadania:

Stwórz klasę Variable, która będzie posiadać metody:
getInt - pobieranie wartości, jako int
getDouble - pobieranie wartości, jako double
getChar - pobieranie wartości, jako znak
getShort - pobieranie wartości, jako short
getUnsigned - pobieranie wartości, jako unsigned
analogiczne settery
Zadaniem klasy jest możliwość przechowywania różnych typów zmiennych. Do zapisu danych w klasie należy użyć jednego pola. Nie można konwertować typow p(int -> double itp), należy uży reinterpret_cast.

Coś do czego udało mi się dojść to tak wyglądający kod. Jest to jedna z wersji, które udało mi się napisać:

Plik main.cpp

#include <iostream>
#include "Variable.h"
using namespace std;


int main()
{
    Variable v1;

    v1.setDouble(487.6);
    cout << v1.getInt() << endl;
  
    v1.setChar('A');
    cout << v1.getInt() << endl;

    v1.setInt(-123);
    cout << v1.getUnsigned() << endl;
     
}

Plik Variable.h

#pragma once
#include <iostream>

class Variable
{
	private:
		double* data;
	

	public:

		void setInt(int value);
		void setDouble(double value);
		void setChar(char value);
		void setShort(short value);
		void setUnsigned(unsigned value);

		int getInt();
		double getDouble();
		char getChar();
		short getShort();
		unsigned getUnsigned();

};

Variable.cpp

#include "Variable.h"
#include <iostream>
using namespace std;

void Variable::setInt(int value)
{
	data = reinterpret_cast <double*>(&value);
}

void Variable::setDouble(double value)
{
	data = reinterpret_cast <double*>(&value);
}

void Variable::setChar(char value)
{
	data = reinterpret_cast <double*>(&value);
}

void Variable::setShort(short value)
{
	data = reinterpret_cast <double*>(&value);
}

void Variable::setUnsigned(unsigned value)
{
	data = reinterpret_cast <double*>(&value);
}

int Variable::getInt()
{
	return *data;
}

double Variable::getDouble()
{
	return *data;
}

char Variable::getChar()
{
	return *data;
}

short Variable::getShort()
{
	return *data;
}

unsigned Variable::getUnsigned()
{
	return *data;
}

Mój pomysł na ten program był taki aby każdy z typów na początku w setterach zmieniać na typ double, ponieważ ten typ to typ, którego rozmiar ma nawięcej bajtów z pozostałych typów danych w tym zadaniu, a następnie chciałem spróbować znowu konwertować typ już na inny w zależności od tego jaki chcemy uzyskać. Próbowałem już wielu rzeczy i cokolwiek od tego momentu kodu bym nie sprobował kończy się źle dlatego piszę tutaj z prośbą o pomoc o jakąś wskazówkę co mógłbym zrobić lub zmienić w moim kodzie. W załącznikach zip z powyższymi plikami.

Dziękuję za wszelką pomoc

Zadanie.zip

5

Wygląda na to, że próbujesz zaimplementować ubogą wersję std::variant.
Pytanie co ma się dziać, jeśli zmienna tego typu zawiera typ inny niż ten który próbujemy pobrać? Twój obecny kod prowadzi do Undefined Behavior w takim wypadku.
Ma być rzucony wyjątek? A może wykonać konwersje jeśli to możliwe? Co w takim wypadku z overflow?

Możesz używać std::variant z C++17? To dużo uprości. Wystarczy opakować ten typ twoją klasą.

#include <variant>

class Variable
{
    using Value = std::variant<int, double, char, short, unsigned>;
    Value value;

public:
    int getInt() const {
        return get<int>();
    }

    double getDouble() const {
        return get<double>();
    }

    char getChar() const {
        return get<char>();
    }

    short getShort() const {
        return get<short>();
    }

    unsigned getUnsigned() const {
        return get<unsigned>();
    }

    template<typename T>
    T get() const {
        return std::get<T>(value);
    }

    void set(int x) {
        value = x;
    }
    void set(double x) {
        value = x;
    }
    void set(char ch) {
        value = ch;
    }
    void set(short x) {
        value = x;
    }
    void set(unsigned x) {
        value = x;
    }
};

https://godbolt.org/z/nEGbqf3oa

Ten twój kod:

void Variable::setInt(int value)
{
	data = reinterpret_cast <double*>(&value);
}

jest źle na wiele sposobów:

  • bo zapamiętujesz wskaźnik do zmiennej, której czas życia sę kończy - w momencie użycia tego wskaźnika masz niezdefiniowane zachowanie
  • reinterpret_cast do typu, który nie występował w danym miejscu to też jest niezdefiniowane zachowanie.
  • nigdzie nie zapamiętujesz jaki tam włąściwie był typ
1
Quba19 napisał(a):

analogiczne settery
Zadaniem klasy jest możliwość przechowywania różnych typów zmiennych. Do zapisu danych w klasie należy użyć jednego pola.

O wiele bardziej bym akceptował zadanie, gdyby jedyne ustawienie było w konstruktorze

Nie można konwertować typow p(int -> double itp), należy uży reinterpret_cast.

Chore, koledzy już mówili

MarekR22 napisał(a):
  • nigdzie nie zapamiętujesz jaki tam włąściwie był typ

To bardzo ważne.
Robiąc to dobrze, można by rzucić wyjątkiem przy probie odczytu wartości, której nie posiadamy

0

Dziękuję wam bardzo za podpowiedzi i wskazówki, spróbuję teraz poprawić mój kod

0
v1.setDouble(487.6);
cout << v1.getInt() << endl;

Zapisanie double'a a czytanie inta jest UB (undefined behaviour, zachowaniem niezdefiniowanym). W praktyce pewnie będzie działać. Ale nie musi.

0

Powinieneś mieć void* data;. Do tego jakiś enum, który mówi który typ trzymamy

Z chatu gpt:

// Enum flag for data type
enum class DataType {
    INT,
    DOUBLE,
    CHAR,
    NONE
};

class Variable {
private:
    void* data;
    DataType typeFlag;

public:
    Variable() : data(nullptr), typeFlag(DataType::NONE) {}

    // Setter and Getter for int
    void setInt(int value) {
        cleanUp();
        data = new int(value);
        typeFlag = DataType::INT;
    }

    int getInt() const {
        checkType(DataType::INT);
        return *reinterpret_cast<int*>(data);
    }

    // Setter and Getter for double
    void setDouble(double value) {
        cleanUp();
        data = new double(value);
        typeFlag = DataType::DOUBLE;
    }

    double getDouble() const {
        checkType(DataType::DOUBLE);
        return *reinterpret_cast<double*>(data);
    }

    // Setter and Getter for char
    void setChar(char value) {
        cleanUp();
        data = new char(value);
        typeFlag = DataType::CHAR;
    }

    char getChar() const {
        checkType(DataType::CHAR);
        return *reinterpret_cast<char*>(data);
    }

    // Destructor to free allocated memory
    ~Variable() {
        cleanUp();
    }

private:
    // Helper function to clean up allocated memory
    void cleanUp() {
        delete data;
        typeFlag = DataType::NONE;
    }

    // Helper function to check if the requested type matches the stored type
    void checkType(DataType requestedType) const {
        if (typeFlag != requestedType) {
            throw std::logic_error("Invalid data type access.");
        }
    }
};
0
slsy napisał(a):
class Variable {
    void cleanUp() {
        delete data;
        typeFlag = DataType::NONE;
    }
};

Czat Gpt popełnia te same błędy co ludzie często te bardziej zawoalowane.
Ten delete data; troszkę mnie niepokoi. Nie jestem pewien czy delete na typie void*, który wskazuje tak naprawdę na int to przypadkiem nie jest niezdefiniowane zachowanie.
Tu mamy tylko Plain Old Data (POD), więc może być ok, ale z typami, które nie są POD to na 100% jest UB.
Zresztą kompilator o tym ostrzega: https://godbolt.org/z/6TGrv87eG - więc bardziej skłaniam się do tego, że nawet z POD jest to UB.

<source>:5:12: warning: deleting 'void*' is undefined [-Wdelete-incomplete]
    5 |     delete v;
      |            ^

Ten kod ma standardowy błąd z brakującymi "copy" i "move" konstruktorami i przypisaniami, które nie są trywialne w tym wypadku (te domyślne generowane przez kompilator są niepoprawne).

0
MarekR22 napisał(a):

Ten delete data; troszkę mnie niepokoi. Nie jestem pewien czy delete na typie void*, który wskazuje tak naprawdę na int to przypadkiem nie jest niezdefiniowane zachowanie.

Też o tym myślałem, ale nie chciało mi się myśleć nad tym jako, że jest to zadanie na studia. Najlepiej byłoby użyć po prostu malloc/free

Ten kod ma standardowy błąd z brakującymi "copy" i "move" konstruktorami i przypisaniami, które nie są trywialne w tym wypadku (te domyślne generowane przez kompilator są niepoprawne).

To prawda, mój błąd

0

Nie można konwertować typow p(int -> double itp), należy uży reinterpret_cast.

Chore, koledzy już mówili

Nie ma w tym nic chorego, wszak konwersja kosztuje.

MarekR22 napisał(a):
  • nigdzie nie zapamiętujesz jaki tam włąściwie był typ

To bardzo ważne.
Robiąc to dobrze, można by rzucić wyjątkiem przy probie odczytu wartości, której nie posiadamy

Można przecież założyć, że posiada się wiedzę o aktualnie przechowywanym typie. Sprawdzenie typu to kolejny, niepotrzebny koszt.

1

Wersja bez std::variant

class Variable {
    enum class StoredType {
        Int,
        Double,
        Char,
        Short,
        Unsigned
    };

    union ValueUion {
        int i;
        double d;
        char c;
        short s;
        unsigned u;
    };

    static constexpr StoredType storedTypeFor(int)
    {
        return StoredType::Int;
    }

    static constexpr StoredType storedTypeFor(double)
    {
        return StoredType::Double;
    }

    static constexpr StoredType storedTypeFor(char)
    {
        return StoredType::Char;
    }

    static constexpr StoredType storedTypeFor(short)
    {
        return StoredType::Short;
    }

    static constexpr StoredType storedTypeFor(unsigned)
    {
        return StoredType::Unsigned;
    }

    ValueUion value { 0 };
    StoredType type { StoredType::Int };

    void checkIsValid(StoredType expected) const
    {
        if (expected != type) {
            throw std::runtime_error { "" };
        }
    }

public:
    int getInt() const
    {
        checkIsValid(StoredType::Int);
        return value.i;
    }

    double getDouble() const
    {
        checkIsValid(StoredType::Double);
        return value.d;
    }

    char getChar() const
    {
        checkIsValid(StoredType::Char);
        return value.c;
    }

    short getShort() const
    {
        checkIsValid(StoredType::Short);
        return value.s;
    }

    unsigned getUnsigned() const
    {
        checkIsValid(StoredType::Unsigned);
        return value.u;
    }

    template <typename T>
    T get() const
    {
        checkIsValid(storedTypeFor(T {}));
        return convertTo<T>();
    }

    template <typename T>
    T convertTo() const
    {
        switch (type) {
        case StoredType::Int:
            return value.i;
        case StoredType::Double:
            return value.d;
        case StoredType::Char:
            return value.c;
        case StoredType::Short:
            return value.s;
        case StoredType::Unsigned:
            return value.u;
        }
        throw std::runtime_error { "" };
    }

    void set(int x)
    {
        type = StoredType::Int;
        value.i = x;
    }
    void set(double x)
    {
        type = StoredType::Double;
        value.d = x;
    }
    void set(char ch)
    {
        type = StoredType::Char;
        value.c = ch;
    }
    void set(short x)
    {
        type = StoredType::Short;
        value.s = x;
    }
    void set(unsigned x)
    {
        type = StoredType::Unsigned;
        value.u = x;
    }
};

https://godbolt.org/z/z4d7fz1K7

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