01. Floating Numbers 1
# 변환 방법
float은 아래와 같은 방법으로 변환된다
# converter
위에서 사용되는 이진법 부동소수점 정의에
스탠다드는 IEEE 754이고
해당 변환을 직접적으로 보여주는 사이트를 활용해서
내부를 확인할 수 있다
02. Floating Numbers 2
# 주의점
float은 근삿값으로 변환이 되기에
equality, comparison을 사용하는 것에 주의해야 한다
double은 float보다는 더욱 정밀하지만
마찬가지로 주의해야 한다
#include <iostream>
#include <cmath>
#include <iomanip>
#include <limits>
int main()
{
const float num1 = 0.3f;
const float num2 = 0.4f;
if (0.7f == num1 + num2)
{
std::cout << "YES";
}
else
{
std::cout << "NO";
}
}
# 해결 방법
아래의 almost_equal과 같은 함수를 만들어주면 된다
int ulp는 유동적으로 scale에 맞게 근사치를 조정하기 위한 부분이다
#include <iostream>
#include <cmath>
#include <iomanip>
#include <limits>
bool almost_equal(float x, float y, int ulp)
{
const float diff = std::fabs(x - y);
return diff <= std::numeric_limits<float>::epsilon() * std::abs(x + y) * ulp
|| diff < std::numeric_limits<float>::min();
}
int main()
{
const float num1 = 0.3f;
const float num2 = 0.4f;
if (almost_equal(0.7f, num1 + num2, 1))
{
std::cout << "YES";
}
else
{
std::cout << "NO";
}
}
# 해결 방법 - 언리얼/유니티
// 언리얼
bool FMath::IsNearlyEqual(float A, float B, float ErrorTolerance = SMALL_NUMBER)
// 유니티
bool Mathf.Approximately(float a, float b)
// 언리얼
float a = 0.1f + 0.2f;
float b = 0.3f;
if (FMath::IsNearlyEqual(a, b, 0.0001f))
{
UE_LOG(LogTemp, Warning, TEXT("a and b are approximately equal"));
}
else
{
UE_LOG(LogTemp, Warning, TEXT("a and b are not equal"));
}
// 유니티
float a = 0.1f + 0.2f;
float b = 0.3f;
if (Mathf.Approximately(a, b))
{
Debug.Log("a and b are approximately equal");
}
else
{
Debug.Log("a and b are not equal");
}
03. std::pair, tuple
pair와 tuple은
둘 다 서로 다른 형식의 데이터를 하나로 묶어서 사용할 수 있는
C++의 템플릿 클래스이다
# pair 예시
enum class ErrorCode
{
NoError,
Error_divide0,
Error_other,
};
std::pair<int, ErrorCode> divide(int a, int b)
{
if (b == 0)
{
return {0, ErrorCode::Error_divide0};
}
return {a / b, 0};
}
# tuple 예시
pair는 2개만 가능하지만
tuple은 2개 이상도 가능하다
#include <iostream>
#include <stdexcept>
#include <string>
#include <tuple>
std::tuple<double, char, std::string> get_student(int id)
{
switch (id)
{
case 0: return { 3.8, 'A', "Lisa Simpson" };
case 1: return { 2.9, 'C', "Milhouse Van Houten" };
case 2: return { 1.7, 'D', "Ralph Wiggum" };
case 3: return { 0.6, 'F', "Bart Simpson" };
}
throw std::invalid_argument("id");
}
int main()
{
// 1-1번째 방법
const auto student0 = get_student(0);
std::cout << "ID: 0, "
<< "GPA: " << std::get<0>(student0) << ", "
<< "grade: " << std::get<1>(student0) << ", "
<< "name: " << std::get<2>(student0) << '\n';
// 1-2번째 방법
const auto student1 = get_student(1);
std::cout << "ID: 1, "
<< "GPA: " << std::get<double>(student1) << ", "
<< "grade: " << std::get<char>(student1) << ", "
<< "name: " << std::get<std::string>(student1) << '\n';
// 2번째 방법
double gpa2;
char grade2;
std::string name2;
std::tie(gpa2, grade2, name2) = get_student(2);
std::cout << "ID: 2, "
<< "GPA: " << gpa2 << ", "
<< "grade: " << grade2 << ", "
<< "name: " << name2 << '\n';
// 3번째 방법
// C++17 structured binding
const auto [gpa3, grade3, name3] = get_student(3);
std::cout << "ID: 3, "
<< "GPA: " << gpa3 << ", "
<< "grade: " << grade3 << ", "
<< "name: " << name3 << '\n';
}
04. std::optional
pair보다 가독성이 높으니 적절히 활용하면 좋다
해당 반환 타입에 문제가 있는지 없는지를 리턴해준다
다만, 해당 타입이 valid/invalid 여부만을 리턴해주기에
더 많은 에러 정보를 리턴해주고 싶다면,
pair 혹은 tuple을 이용하면 된다
또한,
해당 optional이 Valid 한 지 Invalid 한 지의 여부를
구분할 수 있는 정보가 추가되기 때문에
일반적인 변수 사이즈보다 조금 더 크다
# 예시 1
#include <iostream>
#include <optional>
std::optional<int> divide(int a, int b)
{
if (b == 0)
{
return std::nullopt;
}
return a / b;
}
int main()
{
const auto answer = divide(10, 0);
if (answer)
{
std::cout << answer.value() << std::endl;
}
else
{
std::cout << answer.value_or(0) << std::endl;
// answeer.value_or을 통해 직접 값을 지정해 반환 가능
}
}
# 예시 2 - 클래스
#include <iostream>
#include <optional>
class Cat
{
private:
int n = 10;
public:
void print()
{
std::cout << "meow" << std::endl;
}
};
int main()
{
std::optional<Cat> cat;// 실제로 오브젝트가 생성 X
cat = Cat();// 오브젝트 생성 방법 1
std::optional<Cat> cat2{ Cat() };// 오브젝트 생성 방법 2
// 위의 2가지 방식은 임시로 Cat을 만들고 move를 하기에
// 비효율적인 방식
std::optional<Cat> cat3{std::in_place};// 오브젝트 생성 방법 3
// 바로 생성하는 방식
}
05. enum class
enum class는 C++11에서 도입된 열거형(enum)의 새로운 형태이다
기존의 C++의 열거형은 값들이
해당 열거형의 범위 내에서 모두 공유되어
이름 충돌을 초래할 수 있었다
enum class는 이러한 문제를 해결하기 위해 만들어졌다
각 열거형 멤버는 해당 열거형의 범위 내에서만 사용할 수 있으며,
기본적으로 기본 유형과 다르게 취급된다
간단히 말해, enum class는 열거형의 값들을 범위로 묶어주고
각 값들에 대해 별도의 네임스페이스를 제공하여
코드의 가독성과 안전성을 높여준다
enum class는 연산자 오버로딩 또한 가능하다
#include <cstdint>
#include <iostream>
enum color
{
red,
yellow,
green = 20,
blue
};
// enumeration types (both scoped and unscoped) can have overloaded operators
std::ostream& operator<<(std::ostream& os, color c)
{
switch (c)
{
case red: os << "red"; break;
case yellow: os << "yellow"; break;
case green: os << "green"; break;
case blue: os << "blue"; break;
default: os.setstate(std::ios_base::failbit);
}
return os;
}
int main()
{
color col = red;
std::cout << "col = " << col << '\n';
}
06. Union
Union은 하나의 메모리를 공유하면서
읽는 형태를 변경하는 경우에 사용한다
memory saving이 필요한 분야에서 많이 활용된다
하지만 아래와 같은 문제점들이 발생하면서
위험한 경우가 생길 가능성이 높다
# 문제점 1 - undefined behavior
#include <iostream>
struct S// 4 + (4) + 8 = 16
{
int i;// 4
double d;// 8
};
union U// 8
{
int i;// 4
double d;// 8
};
int main()
{
U u;
u.i = 10;
std::cout << u.i << std::endl;
u.d = 0.3;
std::cout << u.d << std::endl;
//std::cout << u.i << std::endl;// undefined behavior
}
# 문제점 2 - Object
오브젝트가 Union에 들어오게 되면
오브젝트의 Constructor/Destructor를 반드시 작성해야 한다
작성하지 않으면, segmentation fault가 발생한다
#include <iostream>
#include <string>
#include <vector>
union S
{
std::string str;
std::vector<int> vec;
~S() {}
};
int main()
{
S s = {"Hello, world"};
std::cout << "s.str = " << s.str << '\n';
s.str.~basic_string();// string의 destructor
new (&s.vec) std::vector<int>;// vector의 constructor
s.vec.push_back(10);
std::cout << s.vec.size() << '\n';
s.vec.~vector();
}
# 해결 방법
union은 위와 같은 위험성으로 인해
아래와 같은 코드에서 사용하는 방법을 사용해서
조금은 덜 위험하게 사용할 수 있다
하지만 여전히 위험한 것은 사실이고
이를 해결하기 위해 std::variant가 등장했다
#include <iostream>
// S has one non-static data member (tag), three enumerator members (CHAR, INT, DOUBLE),
// and three variant members (c, i, d)
struct S
{
enum{CHAR, INT, DOUBLE} tag;// 태그를 통해 추적
union
{
char c;
int i;
double d;
};
};
void print_s(const S& s)
{
switch(s.tag)
{
case S::CHAR: std::cout << s.c << '\n'; break;
case S::INT: std::cout << s.i << '\n'; break;
case S::DOUBLE: std::cout << s.d << '\n'; break;
}
}
int main()
{
S s = {S::CHAR, 'a'};
print_s(s);
s.tag = S::INT;
s.i = 123;
print_s(s);
}
07. std::variant
union을 안전하게 사용하기 위해 도입되었다
type tracking information의 추가적인 공간이 필요하다
매번 호출을 할 때마다 type check를 해줘야 해서
어느 정도 overhead가 발생하지만 사용환경에 따라 적절하게 사용한다면
union보다는 std::variant를 사용하는 것이 좋다
#include <iostream>
#include <variant>
struct S
{
int i;
double d;
float f;
};
union U
{
int i;
double d;
float f;
};
int main()
{
std::variant<int, double, float> v;
std::cout << "S: " << sizeof(S) << std::endl;// 24
std::cout << "U: " << sizeof(U) << std::endl;// 8
std::cout << "V: " << sizeof(v) << std::endl;// 16 (8 + type tracking information)
}
Union의 문제점이었던 undefined behavior가
발생하지 않고
오브젝트가 Union에 들어오는 경우
오브젝트의 Constructor/Destructor를 작성해주지 않아도 된다
#include <iostream>
#include <variant>
#include <string>
#include <vector>
int main()
{
// 문제점 1 해결
std::variant<int, double, float> v;
v = 10;
if (auto pVal = std::get_if<double>(&v))
{
std::cout << *pVal << std::endl;
}
else
{
std::cout << "v is not type double" << std::endl;
}
// 문제점 2 해결
std::variant<std::string, std::vector<int>> sv;
sv = std::string("abc");
std::cout << std::get<std::string>(sv) << std::endl;
sv = std::vector{ 1,2,3 };
}
pair, tuple 혹은 optional을 사용해도 되지만
variant를 사용하면 훨씬 쉽게 코드를 작성할 수 있는 예시는 아래와 같다
enum class ErrorCode
{
NoError,
Error_divide0,
Error_other,
};
std::variant<int, ErrorCode> divide(int a, int b)
{
if (b == 0)
{
return ErrorCode::Error_divide0;
}
return a / b;
}
08. std::any
std::any는
void* 형으로 모든 자료형의 주소를 담을 수 있는 상태에서
type info를 추가적으로 저장함으로써 조금 더 안전하게 사용할 수 있게 된다
#include <any>
#include <iostream>
int main()
{
std::cout << std::boolalpha;
// any type
std::any a = 1;
std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';
a = 3.14;
std::cout << a.type().name() << ": " << std::any_cast<double>(a) << '\n';
a = true;
std::cout << a.type().name() << ": " << std::any_cast<bool>(a) << '\n';
// bad cast
try
{
a = 1;
std::cout << std::any_cast<float>(a) << '\n';
}
catch (const std::bad_any_cast& e)
{
std::cout << e.what() << '\n';
}
// has value
a = 2;
if (a.has_value())
std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';
// reset
a.reset();
if (!a.has_value())
std::cout << "no value\n";
// pointer to contained data
a = 3;
int* i = std::any_cast<int>(&a);
std::cout << *i << '\n';
}
09. Type Punning
어떤 타입을 나타내는 메모리 공간을
다른 타입으로 읽는 경우를 의미한다
Type Punning을 포인터 형태로 구현하면,
unsigned char*, char*, std::byte *를 제외한
포인터 캐스팅은 undefined behavior로 정의된다
따라서 표준적인 방법으로 만드는 방법은
memcpy를 이용하면 된다
compiler optimization은 memcpy를 최적화시켜주기에
실제 메모리 copy는 일어나지 않으면서
안전하게 Type Punning을 이용할 수 있다
C++ 20부터는 Type Punning을
더 편하고 안전하게 사용하기 위한 함수인
bit_cast가 도입되었다
#include <iostream>
#include <cstring>
bool isNeg(float x)
{
unsigned int* ui = (unsigned int*)&x;
return *ui & 0x80000000;
}
bool isNeg_memcpy(float x)
{
unsigned int tmp;
std::memcpy(&tmp, &x, sizeof(x));
return tmp & 0x80000000;
}
bool isNeg_bitcast(float x)
{
return std::_Bit_cast<uint32_t>(x) & 0x80000000;
}
int main()
{
std::cout << isNeg(-1.0f) << std::endl;
std::cout << isNeg_memcpy(-1.0f) << std::endl;
std::cout << isNeg_bitcast(-1.0f) << std::endl;
}
주로 소켓프로그래밍에서 자주 활용된다
#include <iostream>
#include <cstring>
struct S
{
int a;
double d;
float f;
};
void fn(unsigned char* address, std::size_t length)
{
// send Object(패킷)
// or data copy
// or set Object
}
int main()
{
S s;
fn((unsigned char*)&s, sizeof(s));
}
'C++ > [노코프] C++' 카테고리의 다른 글
[C++ NOTE] Exceptions (0) | 2024.04.03 |
---|---|
[C++ NOTE] Functional Programming (0) | 2024.03.31 |
[C++ NOTE] Template (0) | 2024.03.30 |
[C++ NOTE] Smart Pointer (0) | 2024.03.29 |
[C++ NOTE] Inheritance (0) | 2024.03.28 |