일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- conversion constructor
- operator overloading
- C++
- discord bot
- member function pointer
- std::ostream
- return by reference
- 더 지니어스 양면포커
- base from member
- virtual function
- virtual inheritance
- placement new
- c++ basic practice
- virtual destructor
- suffix return type
- diamond inheritance
- new&delete
- dynamic_cast
- std::cout
- vector size
- c++ multi chatting room
- constructor
- this call
- pointer to member data
- std::vector
- increment operator
- vector capacity
- std::endl
- virtual function table
- delete function
- Today
- Total
I'm FanJae.
[C++] Operator Overloading IV 본문
※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.
1. -> 연산자 재정의를 통한 Smart Pointer
#include <iostream>
class Car
{
int color;
public:
~Car() { std::cout << "~Car"; }
void go() { std::cout << "Car Go"};
};
class Ptr
{
Car* obj;
public:
explicit Ptr(Car* p = nullptr) : obj{p} {}
~Ptr() { delete obj;}
Car* operator->() { return obj;}
Car& operator*() { return *obj;}
};
int main()
{
Ptr p ( new Car ) ; // p는 객체, 타입 Ptr
// Ptr의 객체 p가 Car* 역활을 수행
p->go(); // p.operator->()go()
// (p.operator->())->go()
(*p).go();
}
- 이 예제를 보면, Ptr의 객체인 p가 마치 Car*의 역활을 수행하고 있는 것을 볼 수 있다.
- 이처럼, 다른 타입의 포인터 역활을 하는 객체가 스마트 포인터이다.
- p->go();와 같이 불렀을때, 컴파일러가 다음과 같이 변환하여 호출하는 것이다.
(p.operator->())->go()
- (*p).go();와 같이 불렀을때는, 컴파일러가 다음과 같이 변환하여 호출한다.
(p.operator*()).go()
① 스마트 포인터(Smart Pointer)의 원리와 주의 사항
원리 : 연산자와 * 연산자를 재정의 해서 객체를 포인터처럼 사용가능 하게 만든 것
주의사항 : * 연산자는 반드시 Reference(Car &)로 반환 (임시 객체가 생성되지 않게 만들기 위해서)
② Smart Pointer를 사용하는 이유
Car* p = new Car; // 사용후 delete 해야함.
- 이와 같이 할당을 받은 경우에는 반드시 소멸자를 호출해줘야 한다.
- 하지만, Smart Pointer는 객체다. 즉, 블록을 벗어날 때 소멸자가 호출된다.
※ 스마트 포인터는 객체이므로, 생성/복사/대입/소멸의 모든 과정에서 추가 작업을 수행할 수 있다.
※ 일반적으로, 대표적인 활용이 소멸자에서 객체를 자동으로 삭제하는 기능이다.
1-1. 기존 Ptr의 문제점 개선
class Ptr
{
Car* obj;
public:
explicit Ptr(Car* p = nullptr) : obj{p} {}
~Ptr() { delete obj;}
Car* operator->() { return obj;}
Car& operator*() { return *obj;}
};
- 지금 이 Ptr은 Car에 대해서만 받을 수 있다. 따라서, 이를 모든 타입에 대해 활용 가능하도록 Template화를 진행한다.
#include <iostream>
template <typename T>
class Ptr
{
T* obj;
public:
explicit Ptr(T* p = nullptr) : obj{p} {}
~Ptr() { delete obj;}
T* operator->() { return obj;}
T& operator*() { return *obj;}
};
int main()
{
Ptr<int> p1(new int);
*p1 = 10;
std::cout << *p1;
}
- 이를 활용하면 delete를 하지 않아도 알아서 자원을 회수한다.
- 실제 스마트 포인터를 구현하기 위해서는 여러 기능들을 더 구현해야 한다.
- 하지만, 이미 C++ 표준에서 제공하는 스마트 포인터가 존재한다.
1-2. std::shared_ptr
- C++ 표준에서 제공하는 스마트 포인터
- <memory> 헤더
#include <iostream>
class Car
{
int color;
public:
~Car() { std::cout << "~Car" << std::endl; }
void go() { std::cout << "Car Go" << std::endl; }
};
int main()
{
// std::shared_ptr<Car> sp1 = new Car; // error
std::shared_ptr<Car> sp2(new Car); // ok
std::shared_ptr<Car> sp3{new Car}; // ok
sp2->go();
}
- std::shared_ptr<Car> sp1 = new Car;와 같은 것을 허용하지 않는다.
- 즉, 이는 Explicit 생성자 형태임을 알 수 있다.
- std::shared_ptr을 사용하면, 우리가 직접 구현했던 스마트 포인터처럼 자원 회수에 신경쓰지 않아도 된다.
- shared_ptr은 기본으로 다루기에는 개념이 복잡하여, STL 파트에서 따뤄 나눠서 다루고자 한다.
2. 함수 객체(Function Object)
struct Plus
{
int operator()(int a, int b) const
{
return a+b;
}
}
int main()
{
Plus p; // p는 함수가 아니라 객체.
int n1 = p(1, 2); // 객체 p를 함수 처럼 사용
// p.operator(),(1, 2)
int n2 = p.operator()(1, 2);
}
- () 연산자를 재정의 해서 특정 객체를 함수처럼 사용할 수 있다면, 이것을 함수 객체(Function Object)라고 한다.
2-1. 예제의 개선
template <typename T>
struct Plus
{
T operator()(const T& a, const T& b) const
{
return a + b;
}
};
int main()
{
Plus p; // p는 함수가 아니라 객체.
int n1 = p(1, 2); // 객체 p를 함수 처럼 사용
// p.operator(),(1, 2)
int n2 = p.operator()(1, 2);
}
2-2. 함수 객체를 사용하는 것의 이점
- 상태를 가지는 함수
- 클로져 ( Closure )
- 인라인 치환성
- 이름 충돌 방지 ( ADL 회피 )
※ 상태를 가지는 함수 외에는 다소 어려운 개념이 많아서 Basic에서 다루기는 부적합하다고 한다.
2-3. 상태를 가지는 함수란?
#include <iostream>
#include <cstdlib>
int urand()
{
return rand() % 10;
}
int main()
{
for ( int i = 0; i < 10; i++)
std::cout << urand() << ", ";
}
- 위 예제는 0 ~ 9 사이의 임의의 난수를 발생시키는 예지이다.
- 만약 이것이 중복되지 않는 난수를 반환하라고 가정했다고 생각해보자.
① 일반 함수의 문제점
- 보통, 일반 함수는 동작이 있지만 상태라는 것은 존재하지 않는다.
- 즉, 함수가 실행 중에 생성된 데이터를 보관할 장소가 없다.
- 전역변수 등에 보관해야 하는데 전역변수를 사용하는 것은 일반적으로 좋은 코드가 아니다.
※ 이에 반면, 함수 객체는 동작 뿐 아니라 상태도 가질 수 있다.
2-3-1. 함수 객체의 이점
#include <iostream>
#include <cstdlib>
class URandom
{
public:
int operator()()
{
return rand() % 10;
}
};
URandom urand;
int main()
{
for ( int i = 0; i < 10; i++ )
std::print( "{}, ", urand() );
}
- 함수 객체는 operator() 연산자 함수 뿐 아니라, 멤버 데이터, 생성자 등을 가질 수 있다.
- 상태를 가질 수 있다는 얘기는 즉, 멤버 데이터를 가질 수 있다는 뜻이다.
① std::bitset<> template
- 비트를 관리하는 C++ 표준(STL)이 제공하는 타입(템플릿)
#include <iostream>
#include <bitset>
#include <string>
int main()
{
std::bitset<10> bs;
bs.set(5); // 5번째 비트만 1로
bs.set(); // 모든 비트를 1로
bs.reset(3); // 3번째 비트만 0으로
std::cout << bs.test(3) << std::endl;
std::cout << bs.test(5) << std::endl;
std::cout << bs.to_string() << std::endl;
}
- 결과는 아마 이와 같을 것이다.
- 이를 활용하면, 중복 되지 않는 난수를 발생하는 것이 가능할 것이다.
2-3-2. 중복되지 않는 난수 발생기의 완성
#include <iostream>
#include <cstdlib>
#include <bitset>
class URandom
{
std::bitset<10> bs;
bool recycle;
public:
URandom(bool b = false) : recycle{b}
{
bs.set(); // 10 비트 모두를 1로 둔다.
}
int operator()()
{
if ( bs.none() )
{
if ( recycle )
{
bs.set();
}
else
return -1;
}
int v = -1;
while( ! bs.test(v = rand() % 10) );
bs.reset(v);
return v;
}
};
URandom urand;
int main()
{
for ( int i = 0; i < 10; i++ )
std::cout << urand() << ", ";
}
while( ! bs.test(v = rand() % 10) );
- 이 로직은 1이 되어 있는 부분을 찾기 위해서 반복한다.
if ( bs.none() )
{
if ( recycle )
{
bs.set();
}
else
return -1;
}
- 다음과 같이 적어줌으로써, 사용자가 재사용을 원할 경우, 다시 10비트를 모두 1로 바꿔서, 재사용을 가능하게 한다.
2-4. C++ 표준에서 제공하는 함수 객체
#include <iostream>
#include <functional>
int main()
{
std::plus<int> f1;
std::minus<int> f2;
std::modulus<int> f3; // %
std::cout << f1(10,3) << std::endl; // 13
std::cout << f2(10,3) << std::endl; // 7
std::cout << f3(10,3) << std::endl; // 1
}
- 이처럼, () 연산을 재정의 하여 함수를 객체처럼 사용 가능하다.
3. 인자 기반 탐색(Argument Dependent Lookup)
namespace Graphics
{
class Point {};
void draw_pixel(const Point& p1) {}
void set_color(int c) {}
}
int main()
{
Graphics::Point pt;
Graphics::draw_pixel(pt);
draw_pixel(pt);
}
- namespace 안에 있는 daw_pixel를 호출 하지만 아무런 문제가 없다.
① ADL(Argument Dependent Lookup) 이란?
- 함수를 검색할 때 인자가 포함된 namespace는 자동으로 검색에 포함되는 개념이다.
- Koening Lookup이라고도 부른다. (만든 사람이 Andrew Kening이라서)
- 즉, Point는 Graphics에 있기 때문에 draw_pixel을 찾을때, Graphics를 자동으로 검색한다는 것이다.
3-1. ADL을 만든 이유
namespace std;
{
class string{};
string operator+(const string& s1, const string& s2)
{
return s1; // 정확하지 않은 구현
}
}
int main()
{
std::string s1;
std::string s2;
s1 + s2; // operator+(s1, s2)
operator+(s1, s2);
}
- 분명 STL을 명시해 놓은적이 없지만, 정상적으로 실행이 된다.
- 이것이 되는 이유가 ADL을 지원하기 때문이다.
- 만약 이 상황에서 ADL이 없다고 생각해보자.
① ADL이 없으면 어떻게 될까?
std::operator+(s1,s2);
- std::string 객체에 대해서 s1 + s2처럼 사용하는건 불가능하다. namespace를 적을 방법이 없다.
- ADL 덕분에 namespace 안에 있는 타입에 대해서 다양한 연산자 재정의 함수를 사용할 수 있다.
② C++ 표준 함수에서의 ADL
#include <algorithm>
#include <string>
int main()
{
int n1 = 10;
int n2 = 20;
std::string s1 = "AAA";
std::string s2 = "BBB";
std::swap(n1, n2);
std::swap(s1, s2);
swap(n1, n2); // error
swap(s1, s2); // ok
}
- swap(n1, n2); 에 대해서는 에러가 발생한다. 이는, int는 std에 없다. string은 std에 있기에 가능하지만 말이다.
- 함수 인자가 std 이름 공간안에 있는 타입의 객체라면 std::를 붙이지 않아도 사용 가능하다.
- 하지만, 가독성을 위해서 되도록 붙이는 것이 좋다.
'C++ > Basic' 카테고리의 다른 글
[C++] STL Container II (0) | 2024.08.27 |
---|---|
[C++] STL Container I (0) | 2024.08.27 |
[C++] Operator Overloading III (0) | 2024.08.25 |
[C++] Operator Overloading II (0) | 2024.08.24 |
[C++] Operator Overloading I (0) | 2024.08.23 |