Bezpieczne castowanie intów przez przycinanie wartości - Szablon

3

Chcę pod C++11 napisać taki szablon narrow_int_cast


	template<class DestinationType, class OriginalType>
	DestinationType CommonMinimum()
	{
		static_assert(std::is_integral<DestinationType>::value, "Only integer types are supported");
		static_assert(std::is_integral<OriginalType>::value, "Only integer types are supported");

		if (sizeof(DestinationType)>sizeof(OriginalType))
		{
			return static_cast<DestinationType>(CommonMinimum<OriginalType, DestinationType>());
		}
		return std::numeric_limits<DestinationType>::min();
	}
	
	template<class DestinationType, class OriginalType>
	DestinationType CommonMaximum()
	{
		static_assert(std::is_integral<DestinationType>::value, "Only integer types are supported");
		static_assert(std::is_integral<OriginalType>::value, "Only integer types are supported");

		if (sizeof(DestinationType)>sizeof(OriginalType))
		{
			// both type have equal size but have sign difference
			return static_cast<DestinationType>(CommonMaximum<OriginalType, DestinationType>());
		}

		return static_cast<DestinationType>(std::numeric_limits<DestinationType>::max());
	}

 	template<class DestinationType, class OriginalType>
	DestinationType narrow_int_cast(OriginalType x)
	{
		static_assert(std::is_integral<DestinationType>::value, "Only integer types are supported");
		static_assert(std::is_integral<OriginalType>::value, "Only integer types are supported");
		
		const auto minimumValue = CommonMinimum<DestinationType, OriginalType>();
		const auto maximumValue = CommonMaximum<DestinationType, OriginalType>();
		const auto result = std::max<OriginalType>(minimumValue, std::min<OriginalType>(x, maximumValue));
		return static_cast<DestinationType>(result);
	}

Wiem, że coś takiego jest w boost, ale rzuca wyjątek, a ja chcę by wartości były przycinane do poprawnego zakresu. Poza tym projekt nie korzysta z boost-a.
Napisanie tego szablony jeśli sizeof(DestinationType) < sizeof(OriginalType) to nie problem.
Schody zaczynają się gdy sizeof(DestinationType) == sizeof(OriginalType) i typy różnią się tylko znakiem.
Jak na razie nie mam pomysłu jak to rozwiązać, więc liczę na wasz feedback.

PS. Kod pisany na szybkiego nieprzetestowany, więc może mieć bugi. Najpierw muszę dopisać gtest-a do tego, ale liczę na świeże pomysły.

4

Może tak?

#include <iostream>
#include <limits>

template<typename Desired, typename Given>
constexpr Desired narrow_cast(Given arg) {
	static_assert(std::is_integral<Desired>::value, "Only integer types are supported");
	static_assert(std::is_integral<Given>::value, "Only integer types are supported");
	
	auto min = std::numeric_limits<Desired>::min();
	auto max = std::numeric_limits<Desired>::max();
	
	auto different_signs = std::is_signed<Given>::value xor std::is_signed<Desired>::value;
	if(sizeof(Desired) == sizeof(Given) && different_signs && arg < 0) {
		return 0;
	}
	
	return 
		min > arg? min:
		max < arg? max: 
		arg;
}

int main() {
	std::cout << narrow_cast<short>(100000) << std::endl;
	std::cout << narrow_cast<unsigned short>(100000) << std::endl;
	std::cout << narrow_cast<unsigned short>(-100000) << std::endl;
	std::cout << narrow_cast<int>(-1) << std::endl;
	std::cout << narrow_cast<unsigned int>(-1) << std::endl;
	std::cout << narrow_cast<unsigned short>(-1) << std::endl;
	return 0;
}

https://ideone.com/HF4Yr1

32767
65535
0
-1
0
0
0

W sumie ta linijka jest tym co potrzebuje:

auto different_signs = std::is_signed<Given>::value xor std::is_signed<Desired>::value;

W przytoczonym kodzie te return 0 jest bezsensu i daje zły wynik, ale przynajmniej już wiem jak zrobić.

Dzięki

3

Nie jest bez sensu. Po zakomentowaniu return 0 wynik to:

32767
65535
0
-1
4294967295
0

przy porównaniu tej samej wielkości liczb signed i unsigned, signed jest zamieniona na unsigned, więc dla ujemnych masz ogromne wartości dodanie. -1 > 0u == true.

0

Przerobiłem na coś takiego (ustawienie kompilatora nie pozwalają na porównywanie typów singed vs unsigned):

	template<typename DestinationType, typename SourceType>
#if __cplusplus >= 201402L
	// c++14
	constexpr DestinationType narrow_cast(SourceType arg) {
#else
	// c++11 doesn't support variables in const expresion
	DestinationType narrow_cast(SourceType arg) {
#endif
		static_assert(std::is_integral<DestinationType>::value, "Only integer types are supported");
		static_assert(std::is_integral<SourceType>::value, "Only integer types are supported");
		
		if (sizeof(DestinationType) > sizeof(SourceType))
		{
			return static_cast<DestinationType>(arg);
		}
		
		auto different_signes = std::is_signed<SourceType>::value xor std::is_signed<DestinationType>::value;
		auto forceCommonBounds = sizeof(DestinationType) == sizeof(SourceType) && different_signes;
		
		auto min = static_cast<SourceType>(forceCommonBounds ? 0 : std::numeric_limits<DestinationType>::min());
		using signedDestType = typename std::make_signed<DestinationType>::type;
		auto max = static_cast<SourceType>(forceCommonBounds ? std::numeric_limits<signedDestType>::max()
										                     : std::numeric_limits<DestinationType>::max());
		
		return static_cast<DestinationType>(
			min >= arg ? min :
			max <= arg ? max :
						 arg);
	}

gtest do tego

#include "UnitTest.h"
#include "Utils.h"

using namespace std;
using namespace clientsdk;

#if __cplusplus >= 201402L
#define NARROW_CAST_TEST(x, y) static_assert(x == y, "Template narrow_cast const expretion is broken for: " #x )
#else
#define NARROW_CAST_TEST(x, y) EXPECT_EQ(y, x) << #x
#endif
TEST(UtilsTest, narrow_castTempalteTest)
{
	NARROW_CAST_TEST(narrow_cast<short>(100000), 32767);
	NARROW_CAST_TEST(narrow_cast<unsigned short>(100000), 65535u);
	NARROW_CAST_TEST(narrow_cast<unsigned short>(-100000), 0u);
	NARROW_CAST_TEST(narrow_cast<short>(-100000), -32768);
	NARROW_CAST_TEST(narrow_cast<int>(-1), -1);
	NARROW_CAST_TEST(narrow_cast<int>(4000000000u), 2147483647);
	NARROW_CAST_TEST(narrow_cast<unsigned int>(4000000000u), 4000000000u);
	NARROW_CAST_TEST(narrow_cast<unsigned int>(-2000000000), 0u);
	NARROW_CAST_TEST(narrow_cast<unsigned int>(1), 1u);
	NARROW_CAST_TEST(narrow_cast<unsigned short>(-1), 0u);
}

Może jeszcze będę kombinował z szablonami, żeby pod C++11 to było constexpr.

0

Jeśli zmienne zamienisz na stałe (constexpr) wewnątrz szablonu, to w C++11 constexpr powinno również przejść.

2

Jak dla mnie ten warunek jest za wcześnie:

if (sizeof(DestinationType) > sizeof(SourceType))
{
    return static_cast<DestinationType>(arg);
}

Przykład:

short a = -1111;
cout << narrow_cast<unsigned long>(a);
0

Po wszystkich poprawkach i uwzględniając przypadłości niektórych kompilatorów:

	template<typename DestinationType, typename SourceType>
#if __cplusplus >= 201402L
	// c++14
	constexpr DestinationType narrow_cast(SourceType arg)
#else
	// c++11 doesn't support variables in const expresion
	DestinationType narrow_cast(SourceType arg)
#endif
	{
		static_assert(std::is_integral<DestinationType>::value, "Only integer types are supported");
		static_assert(std::is_integral<SourceType>::value, "Only integer types are supported");
		
		auto different_signes = (std::is_signed<SourceType>::value != std::is_signed<DestinationType>::value);
		(void)different_signes; // silence visual studio false positive warring
		auto forceCommonBounds = sizeof(DestinationType) == sizeof(SourceType) && different_signes;
		
		auto min = static_cast<SourceType>(forceCommonBounds ? 0 : std::numeric_limits<DestinationType>::min());
		using signedDestType = typename std::make_signed<DestinationType>::type;
		auto max = static_cast<SourceType>(forceCommonBounds ? std::numeric_limits<signedDestType>::max()
										                     : std::numeric_limits<DestinationType>::max());
		
		return static_cast<DestinationType>(
			min >= arg ? min :
			max <= arg ? max :
						 arg);
	}

Jeśli VS stwierdzi, że sizeof(DestinationType) != sizeof(SourceType) to wtedy different_signes jest uznawane za nieużywane, bo koniunkcja jest rozstrzygnięta na pierwszym argumencie na etapie kompilacji :).

0

Mam wersję constexpr działająca pod C++11
http://melpon.org/wandbox/permlink/s5OYTU85ZODhjmeD

#include <iostream>
#include <limits>

template<typename DestinationType, typename SourceType, bool commonSignes, bool DestSourceEqual>
struct _CommonLimits
{
    static constexpr SourceType max()
    {
        return static_cast<SourceType>(std::numeric_limits<DestinationType>::max());
    }
    static constexpr SourceType min()
    {
        return static_cast<SourceType>(std::numeric_limits<DestinationType>::min());
    }
};

template<typename DestinationType, typename SourceType>
struct _CommonLimits<DestinationType, SourceType, false, false>
{
    static constexpr SourceType max()
    {
        return static_cast<SourceType>(std::numeric_limits<DestinationType>::max());
    }
    static constexpr SourceType min()
    {
        return 0;
    }
};

template<typename DestinationType, typename SourceType>
struct _CommonLimits<DestinationType, SourceType, false, true>
{
    static constexpr SourceType max()
    {
        return static_cast<SourceType>(std::numeric_limits<typename std::make_signed<DestinationType>::type>::max());
    }
    static constexpr SourceType min()
    {
        return 0;
    }
};

/**
 * Used to do integer casting in such way that if argument value exceeds destination type range
 * result value is returns respctivly maximum or minimum values of result type.
 */
template<typename DestinationType, typename SourceType>
constexpr DestinationType narrow_cast(SourceType arg)
{
    static_assert(std::is_integral<DestinationType>::value, "Only integer types are supported");
    static_assert(std::is_integral<SourceType>::value, "Only integer types are supported");

    using HelperType = _CommonLimits<
            DestinationType,
            SourceType,
            std::is_signed<SourceType>::value == std::is_signed<DestinationType>::value,
            (sizeof(DestinationType) >= sizeof(SourceType))>;

    return static_cast<DestinationType>(
                                        HelperType::min() >= arg ? HelperType::min() :
                                        HelperType::max() <= arg ? HelperType::max() :
                                        arg);
}
1

Dobra teraz miałem czas by nad tym porządnie przysiąść.
Zaakceptowany kod nie przechodzi wszystkich testów: https://ideone.com/8VLxxT (dopisałem sporo i chyba pokryłem wszystkie przypadki brzegowe).

A tu kod, który się buduje pod C++11 i przechodzi wszystkie testy poprawnie. Jest też poprawka na VS starsze niż 2015, które nie wspierają constexpr.

#if defined(_MSC_VER) && _MSC_VER < 1900
#define CONST_EXPPR
#else
#define CONST_EXPPR constexpr
#endif

	/*
	 * Helper tempaltes which returns common minimum and maximum values for integer types
	 */
	template<typename DestinationType, typename SourceType, bool commonSignes, bool DestSourceEqual>
	struct HelperCommonLimits
	{
		static CONST_EXPPR SourceType max()
		{
			return static_cast<SourceType>(std::numeric_limits<DestinationType>::max());
		}
		static CONST_EXPPR SourceType min()
		{
			return static_cast<SourceType>(std::numeric_limits<DestinationType>::min());
		}
	};
	
	template<typename DestinationType, typename SourceType>
	struct HelperCommonLimits<DestinationType, SourceType, false, false>
	{
		static CONST_EXPPR SourceType max()
		{
			return static_cast<SourceType>(std::numeric_limits<DestinationType>::max());
		}
		static CONST_EXPPR SourceType min()
		{
			return 0;
		}
	};
	
	template<typename DestinationType, typename SourceType>
	struct HelperCommonLimits<DestinationType, SourceType, false, true>
	{
		static CONST_EXPPR SourceType max()
		{
			return static_cast<SourceType>(std::numeric_limits<typename std::make_signed<DestinationType>::type>::max());
		}
		static CONST_EXPPR SourceType min()
		{
			return 0;
		}
	};
	
	/*
	 * Helper template used to detect if size on destination type is bigger.
	 * If signs are same than simple casting is enough.
	 * if sings are different zero minimum is applied
	 */
	template<typename DestinationType, typename SourceType, bool singedToUnsigned, bool DestIsBigger>
	struct HelperNorrowCast
	{
		static CONST_EXPPR DestinationType Cast(SourceType arg)
		{
			using HelperType = HelperCommonLimits<DestinationType,
												SourceType,
												std::is_signed<SourceType>::value == std::is_signed<DestinationType>::value,
												(sizeof(DestinationType) == sizeof(SourceType))>;
			
			return static_cast<DestinationType>(
												HelperType::min() >= arg ? HelperType::min() :
												HelperType::max() <= arg ? HelperType::max() :
												arg);
		}
	};
	
	template<typename DestinationType, typename SourceType>
	struct HelperNorrowCast<DestinationType, SourceType, false, true>
	{
		static CONST_EXPPR DestinationType Cast(SourceType arg)
		{
			return static_cast<DestinationType>(arg);
		}
	};
	
	template<typename DestinationType, typename SourceType>
	struct HelperNorrowCast<DestinationType, SourceType, true, true>
	{
		static CONST_EXPPR DestinationType Cast(SourceType arg)
		{
			return static_cast<DestinationType>(arg >= 0 ? arg : 0);
		}
	};

	/**
	 * Used to do integer casting in such way that if argument value exceeds destination type range
	 * result value is returns respctivly maximum or minimum values of result type.
	 */
	template<typename DestinationType, typename SourceType>
	CONST_EXPPR DestinationType narrow_cast(SourceType arg)
	{
		static_assert(std::is_integral<DestinationType>::value, "Only integer types are supported");
		static_assert(std::is_integral<SourceType>::value, "Only integer types are supported");
		
		return HelperNorrowCast<DestinationType, SourceType,
				std::is_signed<SourceType>::value && std::is_unsigned<DestinationType>::value,
				(sizeof(DestinationType) > sizeof(SourceType))>::Cast(arg);
	}

W sumie to nie spodziewałem się, że ten w miarę prosty problem okaże się aż tak zakręcony.

0
MarekR22 napisał(a):

Bezpieczne castowanie intów przez przycinanie wartości

Rozumiem że chodzi ci o nasycenie (saturację), ale nie nazywałbym tego „bezpiecznym”, bo różne rzeczy są bezpieczne w zależności od zastosowania.

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