Przeładowanie operatora += w C++20

0
template<typename T>
class Test
{
private:
	T Data = static_cast<T>(0);
public:
	operator const T& () const
	{
		return Data;
	}

	const T& operator+=(const T& tValue)
	{
		return Data += tValue;
	}
};

int main()
{
	Test<long> T;
	T += 21;

	return 0;
}

W Visual Studio CE (C++20) coś takiego się nie skompiluje, bo wywala błąd `C2666: Test<long>::operator +=: overloaded functions have similar conversions', wskazując na konflikt z wbudowanym (built-in) operatorem - operator+=(T, int).

Ktoś wie, jak to rozwiązać?

1

podam odpowiedź, i też pytanie pomocnicze, dlaczego założyłeś że 21 to long?

template<typename T>
class Test
{
private:
	T Data = static_cast<T>(0);
public:
	operator const T& () const
	{
		return Data;
	}

	const T& operator+=(const T& tValue)
	{
		return Data += tValue;
	}
};

int main()
{
	Test<long> T;
    long x = 21; // pewnie static_cast<long>(21) też ok
	T += x;

	return 0;
}

edit:co więcej kompilator sam ci powiedział co nie halo

<source>(12): note: could be 'const T &Test<T>::operator +=(const T &)'
        with
        [
            T=long
        ]
<source>(21): note: or       'built-in C++ operator+=(T, int)'
        with
        [
            T=long
        ]
0

No właśnie nie do końca to rozumiem. Tym bardziej, że dla T = int się kompiluje, ale dla innego dowolnego skalara, np. float czy long, już nie. Co więcej, wywalenie operator const T& () const też eliminuje problem, więc mam wrażenie, że to tutaj się coś gryzie. No ale tam jest przecież zwracany const reference, więc nie wiem z czym kompilator ma problem. Gdyby to była po prostu referencja, to wtedy T += 21 mogłoby zostać zinterpretowane dwojako:

  1. Skorzystaj z const T& operator+=(const T& tValue).
  2. Skorzystaj z operator const T& () const, a potem na zwróconej przez tą funkcję wartości (Data = long), wykonaj wbudowaną operację +=.

No ale tutaj zwraca const, więc opcja nr 2 odpada i nie powinno być niejednoznaczności...?

Da się to rozwiązać jakoś bez castowania? Potrzebuję jak najbardziej naturalnego wrappera na skalar, w którym mogę zarządzać tym, co się dzieje z danymi po ich nadpisaniu (chodzi o każdorazowe kopiowanie ich do wewnętrznego bufora). Chciałbym, żeby to funkcjonowało jak np. std::atomic. Ostatecznie mogę zwracać kopię obiektu zamiast referencji, ale wolałbym tego uniknąć.

3

To mi wygląda na bug w VS.
https://godbolt.org/z/srcEK37KG

Proste obejście podczas użycia to:

T += 21l;

Przez co nie nastąpi domyślna konwersja z int do long int.

W momencie użycia masz już pełną instancję szablonu, więc operator+= powinien się zachowywać jak normalna funkcja, a takim wypadku ta konwersja jest jak najbardziej poprawna.

Żeby to obejść, można uczynić z tego operatora szablon i ograniczyć przyjmowane typy za pomocą konceptów:

template <typename T>
class Test {
private:
    T Data = static_cast<T>(0);

public:
    operator const T&() const
    {
        return Data;
    }

    template<std::convertible_to<T> U>
    auto operator+=(const U& tValue) -> const T&
    {
        return Data += tValue;
    }
};

https://godbolt.org/z/3e5518333
albo jeszcze inaczej (kwestia gustu):
https://godbolt.org/z/95jG7bdKh


Jeszcze małe czepialstwo. Twój kod jest poprawny formalnie, ale nie trzyma się standardów jak należy używać przeładowywania operatorów.
Żeby nie utrudniać innym czytanie/rozumienie kodu to powinno być tak:

    auto operator+=(const std::convertible_to<T> auto& tValue) -> Test&
    {
        Data += tValue;
        return *this;
    }

W tej formie bug też się odtwarza: https://godbolt.org/z/Wq4ca8n86

0

Dzięki za pomoc, wyjaśnienia i wskazówki odnośnie kodu, bo wszystkie okazały się trafne i wartościowe. Od siebie dodam, że potestowałem jeszcze troszkę i okazało się, że taki zupełnie najprostszy fix to zmiana z:

template<typename T>
class Test
{
private:
	T Data = static_cast<T>(0);
public:
	operator const T& () const
	{
		return Data;
	}

	const T& operator+=(const T& tValue) //pobiera const T& jako parametr.
	{
		return Data += tValue;
	}
};

na

template <typename T>
class Test 
{
private:
	T Data = static_cast<T>(0);

public:
	operator const T& () const
	{
		return Data;
	}

	const T& operator+=(const auto& tValue) //pobiera const auto& jako parametr.
	{
		return Data += tValue;
	}
};

Zakładam, że w tym drugim przypadku kompilator robi sobie niejawną konwersję i jest zadowolony.

PS. Ta notacja ze zwracanym typem po -> rzeczywiście jest nadal w powszechnym użyciu? Myślałem, że odkąd rozszerzyli możliwości auto w C++14, taki zapis stał się nadmiarowy.

3
Crow napisał(a):

Dzięki za pomoc, wyjaśnienia i wskazówki odnośnie kodu, bo wszystkie okazały się trafne i wartościowe. Od siebie dodam, że potestowałem jeszcze troszkę i okazało się, że taki zupełnie najprostszy fix to zmiana z:
...................
na

template <typename T>
class Test 
{
private:
	T Data = static_cast<T>(0);

public:
	operator const T& () const
	{
		return Data;
	}

	const T& operator+=(const auto& tValue) //pobiera const auto& jako parametr.
	{
		return Data += tValue;
	}
};

Zakładam, że w tym drugim przypadku kompilator robi sobie niejawną konwersję i jest zadowolony.

To źle zakładasz.
Takie użycie auto tworzy ci po prostu szablon, który przyjmuje dowolny typ. To jest równoważne temu:

    template<typename U>
 	const T& operator+=(const U& tValue)
 	{
 		return Data += tValue;
 	}

W moim rozwiązaniu, dodanie konceptu da czytelniejszy bład jeśli szablon zostanie użyty z niewłaściwym typem.
Równocześnie moje rozwiązanie pozostawia ci okienko na dodanie przeładowań tego operatora.

Niejawna konwersja w tym wypadku jest wykonywana wewnątrz szablonu co nie ma wpływu na sposób użuycia szablonu funkcji.
W oryginalnym kodzie niejawna konwersja jest/powinna być wykonywana w ramach dopasowywania argumentu do wywołania funkcji (w ramach overload resolution).

PS. Ta notacja ze zwracanym typem po -> rzeczywiście jest nadal w powszechnym użyciu? Myślałem, że odkąd rozszerzyli możliwości auto w C++14, taki zapis stał się nadmiarowy.

C++14 auto return type, może i jest wygodne, ale jest też niebezpieczne, bo nagle możesz mieć typ zwracany niezgodny z oczekiwaniem. Osobiście preferuje podawać jawnie typ zwracany.

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