C++ 고급 문법 테크닉 - C++ 중요 기본 문법 다시보기[2]
C++ 버전과 역사에 대해서 알아보고, 기본 문법의 This call, New, Name mangling, Conversion 등에 대해 알아보겠습니다.
About C++
- www.isocpp.org : cpp 표준에서 운영하는 사이트
- 표준 cpp 발전 흐름 : C++98/04 -> C++11 -> C++14 -> C++17 -> C++20
This call
-
멤버함수의 호출원리
- 멤버 함수는 1번째 인자로 객체의 주소(this) 가 암시적으로 추가된다. ( 개발자는 볼 수 없음 )
- 전달되는 객체의 주소는 ecx 레지스터로 전달
- static 멤버 함수는 인자로 객체의 주소(this) 가 추가되지 않는다. 그리하여 static 멤버 함수는 멤버 변수에 접근 할 수 없는 오류가 발생하는 것(this가 없기 때문 but 우회하는 트릭이 있음)
- 멤버 함수에 this가 암시적으로 전달 되는 것을 this call이라고 함.
class Point { int x = 0, y = 0; public: void set(int a, int b) // void set(Point* const this, int a, int b) { x = a; // this->x = a; y = b; // this->y = b; } static void foo(int a) // void foo( int a) { x = a; // this->x = a; 변경해야 하는데.. this 가 없다. error } }; // cl this1.cpp /FAs => this1.asm // g++ this1.cpp -S => this1.s int main() { Point::foo(10); // push 10 // call Point::foo Point p1; Point p2; // this call p1.set(10, 20); // set(&p1, 10, 20) // push 20 // push 10 // lea ecx, &p1 rcx, &p1 // 객체 주소는 레지스터로 // call Point::set }
-
멤버함수 포인터
// 핵심 1. 일반 함수 포인터에 멤버함수의 주소를 담을수 없다. // 핵심 2. 일반 함수 포인터에 static 멤버함수의 주소를 담을수 있다. // 핵심 3. 멤버 함수 포인터 모양과 사용법. .* (p->*f3)() class Dialog { public: void Close() {} }; void foo() {} int main() { void(*f1)() = &foo; f1(); // void(*f2)() = &Dialog::Close; // error. this 가 추가되는 함수. void(Dialog::*f3)() = &Dialog::Close; // ok.. 멤버 함수 포인터. //f3(); // error. 객체(this)가 없다. Dialog dlg; //dlg.f3(); // dlg.Close()의 의미.. 하지만..f3이라는 멤버를 찾게된다.- error //dlg.*f3(); // ".*" : pointer to member operator // error. 연산자 우선순위 문제.. (dlg.*f3)(); // ok.. dlg.Close(); }
-
예제 1 - Thread 클래스 만들기
#include <iostream> #include <windows.h> using namespace std; // 라이브러리 내부 클래스 class Thread { public: void run() { CreateThread(0, 0, threadMain,(void*)this, 0, 0); } // 1. 아래 함수는 반드시 static 함수 이어야 합니다. // 2. 아래 함수는 this가 없다. 그래서 함수 인자로 // this를 전달하는 기술. static DWORD __stdcall threadMain(void* p) { Thread* const self = static_cast<Thread*>(p); self->Main(); // Main( self ) //Main(); // 가상함수 호출 // this->Main() => Main( this) 로 변해야 한다. return 0; } virtual void Main() // void Main(Thread* const this) {} }; // 라이브러리 사용자 코드 class MyThread : public Thread { public: virtual void Main() { cout << "스레드 작업" << endl; } }; int main() { MyThread t; t.run(); // 이순간 스레드가 생성되어서 // 가상함수 Main()을 실행해야 합니다. getchar(); }
-
예제 2 - 타이머 개념을 사용해서 Clock 클래스 만들기
#include <iostream> #include <string> #include <map> using namespace std; class Clock { static map<int, Clock*> this_map; string name; public: Clock(string n) : name(n) {} void start(int ms) { int id = set_timer(ms, timerHandler ); this_map[id] = this; } // 핵심 1. 아래 함수는 반드시 static 멤버 이어야 합니다. // 인자가 1개 필요한데(id) non static 멤버 함수는 this가 암시적으로 전달 되기 때문 static void timerHandler(int id) { Clock* const self = this_map[id]; //cout << name << endl; // this->name cout << self->name << endl; } }; map<int, Clock*> Clock::this_map; int main() { Clock c1("A"); Clock c2("\tB"); c1.start(1000); // 1000ms 에 한번씩 이름출력 c2.start(500); // 500ms 에 한번씩 이름출력 process_message(); }
-
다중 상속과 this
#include <iostream> using namespace std; class A { int a; public: void fa() { cout << this << endl; } }; class B { int b; public: void fb() { cout << this << endl; } }; class C : public A, public B { int c; }; int main() { C obj; cout << &obj << endl; // 1000 A* pA = &obj; B* pB = &obj; // &obj + sizeof(A) obj.fa(); // fa(&obj) cout << this => 1000 obj.fb(); // fb(&obj + sizeof(A)) cout << this => 1004 cout << &obj << endl; // 1000 cout << pA << endl; // 1000 cout << pB << endl; // 1004 void (C::*f)(); // 멤버 함수 포인터 ( 크기가 다중상속 일 때 커질 수 있음 ) cout << sizeof(f) << endl; // 32bit 환경에서 8 f = &C::fa; // f = { fa 주소, 0 }; f = &C::fb; // f = { fb 주소, sizeof(A)}; (obj.*f)(); // f.함수주소( &obj + f.this offset ) }
-
멤버 변수 포인터
#include <iostream> using namespace std; class Point { public: int x, y; }; int main() { int n = 10; int* p1 = &n; //void(Point::*f)() = &Point::print; // 멤버 함수 포인터 int Point::*p2 = &Point::x; // 멤버 변수 포인터 // 0 int Point::*p3 = &Point::y; // 4 // C의 offset_of //cout << p3 << endl; printf("%d, %d\n", p2, p3); Point pt; pt.y = 10; pt.*p3 = 20; cout << pt.y << endl; // 20 }
Const Member Function
-
상수 멤버 함수의 필요성 : 변수 값을 변경하지 않는 모든 “멤버 함수"는 “상수 멤버함수"를 권장
-
const 객체를 사용하는 코드에서 대상의 구현이 .h와 .cpp로 나눠져 있을 때 .h 파일만 include 하여도 상수 멤버 함수 속성을 통해 에러를 발생시킬 지 판단 가능
#include <iostream> using namespace std; class Point { public: int x, y; Point(int a = 0, int b = 0) : x(a), y(b) {} void set(int a, int b) { x = a; y = b; } void print() //const // const를 붙이면 상수 멤버 함수 { // x = 10; // error. 모든 멤버를 상수 취급한다. cout << x << ", " << y << endl; } }; int main() { const Point p(1, 1); p.x = 10; // error p.set(10, 20); // error p.print(); // ok or error. }
-
상수 객체는 상수 멤버 함수만 호출 가능
// 객체의 상태를 변경하지 않은 모든 멤버함수는(getxxx) // 반드시 const 멤버 함수가 되어야 한다. struct Rect { int ox, oy, width, height; Rect(int x = 0, int y = 0, int w = 0, int h = 0) : ox(x), oy(y), width(w), height(h) {} int getArea() const { return width * height; } }; void foo( const Rect& r) // call by value 보다는 const & 가 좋다. { int n = r.getArea(); // error. } int main() { Rect r(0, 0, 10, 10); int n = r.getArea(); // ok. foo(r); }
-
-
논리적 상수성 : 상수 멤버 함수에서 변할 수 있는 대상을 지정 할 수 있음
- mutable 키워드 사용
#include <iostream> using namespace std; class Point { int x, y; // mutable 멤버 변수 : 상수멤버함수 안에서도 값을 변경가능. mutable char cache[16]; mutable bool cache_valid = false; public: Point(int a = 0, int b = 0) : x(a), y(b) {} // 캐싱된 값을 사용하는 상수 멤버함수 toString 구현 const char* toString() const { if (cache_valid == false) { sprintf(cache, "%d, %d", x, y); cache_valid = true; } return cache; } }; int main() { Point p1(1, 1); cout << p1.toString() << endl; cout << p1.toString() << endl; }
- 데이터 객체 포인터 사용
#include <iostream> using namespace std; struct Cache { char cache[16]; bool cache_valid = false; }; class Point { int x, y; Cache* pCache; public: Point(int a = 0, int b = 0) : x(a), y(b) { pCache = new Cache; } const char* toString() const { if (pCache->cache_valid == false) { sprintf(pCache->cache, "%d, %d", x, y); pCache->cache_valid = true; } return pCache->cache; } ~Point() { delete pCache; } }; int main() { Point p1(1, 1); cout << p1.toString() << endl; cout << p1.toString() << endl; }
New
-
new vs operator new
- new : 메모리 할당 + 생성자 호출
- operator new : 메모리만 할당
- delete도 같은 방식으로 동작(소멸자 관련)
#include <iostream> using namespace std; class Point { int x, y; public: Point() { cout << "Point()" << endl; } ~Point() { cout << "~Point()" << endl; } }; int main() { /* // malloc : 메모리만 할당 Point* p1 = (Point*)malloc(sizeof(Point)); free(p1); // new : 메모리 할당 + 생성자 호출 Point* p2 = new Point; delete p2; */ Point* p1 = static_cast<Point*>( operator new(sizeof(Point))); operator delete(p1); }
-
placement new : 생성자를 호출하기 위해 표준에서 만든 new
#include <iostream> using namespace std; class Point { int x, y; public: Point() { cout << "Point()" << endl; } ~Point() { cout << "~Point()" << endl; } }; /* cpp 표준에서 생성자를 호출하는 표현 void* operator new(size_t sz, void* p) { return p; } */ int main() { Point p; //new Point; // 인자가 1개인 operator new()호출. new(&p) Point; // 인자가 2개인 operator new()호출. // 메모리 할당이 아닌 생성자를 // 명시적으로 호출하는 코드 p.~Point(); }
-
생성자(소멸자)의 명시적 호출이 필요한 이유
- 객체를 힙에 10개 만들 때 (활용 -> stl의 vector 생성시)
#include <iostream> using namespace std; class Point { int x, y; public: Point(int a, int b) : x(a), y(b) { cout << "Point(int, int)" << endl; } }; int main() { // Point 객체를 힙에 한개 만들고 싶다. Point* p1 = new Point(0, 0); // ok. // Point 객체를 힙에 10개 만들고 싶다. //Point* p2 = new Point[10]; // error. // 1. 메모리만 먼저 힙에 할당 Point* p2 = static_cast<Point*>(operator new(sizeof(Point) * 10)); // 2. 할당한 메모리에 객체를 생성(생성자 호출) for (int i = 0; i < 10; i++) new(&p2[i]) Point(0,0); // 3. 소멸자 호출 for (int i = 9; i >= 0; i--) p2[i].~Point(); // 4. 메모리 해지. operator delete(p2); // 활용 stl vector vector<Point> v(10, Point(0,0)); }
- stl의 vector 관련 추가 설명 -> 소멸자의 명시적 호출이 필요 한 케이스
#include <iostream> #include <vector> using namespace std; int main() { vector<int> v(10, 0); v.resize(7); cout << v.size() << endl; // 7 cout << v.capacity() << endl; // 10 // DBConnect : 생성자에서 DB 접속을 한다고 가정 vector<DBConnect> v2(10); v2.resize(7); // 메모리는 제거하지 않지만 // 줄어든 객체의 소멸자는 호출해야한다 v2.resize(8); // 새로운 객체에 대한 메모리는 있다 // 하지만 생성자를 호출해서 // 다시 DB 접속을 해야 한다. }
Name mangling
컴파일러가 심볼의 이름을 변경하는 현상
-
name mangling : 함수 오버로딩, namespace, template
int square(int n) // 어셈블리 함수 이름 : squarei() { return n * n; } double square(double d) // squared() { return d * d; } int main() { square(3); // squarei(3) square(3.3); // squared(3.3) }
-
extern “C” : C 컴파일러에서는 name mangling 규칙이 적용되지 않아 c 파일을 c++ 컴파일러에서 사용시 링킹 오류를 막기 위한 방법
// 함수 선언 헤더 파일에서 __cplusplus와 exten "C"를 조합해서 사용 // square.h //extern "C" // C++ 컴파일러에게 C 처럼 해석해 달라. #ifdef __cplusplus extern "C" { #endif int square(int); #ifdef __cplusplus } #endif
-
같은 이름의 함수와 함수주소
#include <cstdio> int square(int n) { return n * n; } double square(double d) { return d * d; } void foo(int a) { } int main() { //printf("%p\n", &square); // error //printf("%p\n", static_cast<int(*)(int)>(&square)); // ok. //auto p = □ // error int(*f)(int) = □ // ok void(*f1)(int) = &foo; // ok. 함수 주소 꺼내기 void(*f2)(int) = foo; // 함수 이름은 함수 주소로 암시적 형변환 typedef void(*PF)(int); // 함수 포인터 타입 typedef void F(int); // 함수 타입.. cout << typeid(&foo).name() << endl; // void(*)(int) cout << typeid(foo).name() << endl; // void(int) }
Constructor
-
생성자 호출순서
- 자식 클래스 생성시 부모 클래스의 생성자가 먼저 호출된다.
- 부모 클래스의 생성자는 항상 디폴트 생성자가 호출된다.
- 부모 클래스의 디폴트 생성자가 없는 경우 자식 클래스 객체를 만들 수 없다.
- 부모 클래스의 다른 생성자를 호출하려면 자식 클래스 생성자의 초기화 리스트에서 명시해야 한다.
#include <iostream> using namespace std; class Base { public: // Base() { cout << "B()" << endl; } Base(int a) { cout << "B(int)" << endl; } ~Base() { cout << "~B()" << endl; } }; class Derived : public Base { public: Derived() : Base(0) { cout << "D()" << endl; } Derived(int a): Base(a) { cout << "D(int)" << endl; } ~Derived() { cout << "~D()" << endl; } }; int main() { //Derived d; Derived d(5); }
class Point { int x, y; public: // Point() : x(0), y(0) {} Point(int a, int b) : x(a), y(b) {} }; class Rect { Point p1; Point p2; public: Rect() : p1(0,0), p2(0,0) //: p1(), p2() { } }; int main() { Rect r; // 순서 p1 생성자 -> p2 생성자 -> Rect 생성자 }
-
멤버를 가지고 있는 상속관계 생성자 호출 순서
- 부모 클래스 멤버의 생성자
- 부모 클래스의 생성자
- 자식 클래스 멤버의 생성자
- 자식 클래스의 생성자
#include <iostream> using namespace std; struct BM { BM() { cout << "BM()" << endl; } }; struct DM { DM() { cout << "DM()" << endl; } }; struct Base { BM bm; int x; Base() { cout << "Base()" << endl; } }; struct Derived : public Base { DM dm; int y; Derived() : dm(), Base() { cout << "Derived()" << endl; } }; int main() { Derived d; // BM() -> Base() -> DM() -> Derived() }
-
생성자 호출순서 관련 주의사항
mystream(int sz) : buf(sz), stream(buf) {}
이렇게 buf를 초기화 하는 것으로 표현 해도 절대적 초기화 순서에 의해 버그가 발생함
#include <iostream> using namespace std; struct stream_buf { stream_buf(size_t sz) { cout << "stream_buf" << endl; } }; // 생성자로 버퍼를 받는 객체 struct stream { stream(stream_buf& buf) { cout << "stream : using stream_buf" << endl; } }; // 버퍼를 가지고 있는 객체 struct mystream : public stream { stream_buf buf; public: mystream(int sz) : buf(sz), stream(buf) {} }; int main() { // stream_buf buf(1024); // stream st(buf); mystream mst(1024); }
-
해결 방법, 관리 클래스 만들고 다중상속을 이용해서 초기화 순서를 제어 (cpp 표준 스트림에서 사용 방식)
#include <iostream> using namespace std; struct stream_buf { stream_buf(size_t sz) { cout << "stream_buf" << endl; } }; struct stream { stream(stream_buf& buf) { cout << "stream : using stream_buf" << endl; } }; struct buf_manager { protected: stream_buf buf; public: buf_manager(size_t sz) : buf(sz) {} }; struct mystream : public buf_manager, public stream { public: mystream(size_t sz) : buf_manager(sz), stream(buf) {} }; int main() { mystream mst(1024); }
-
생성자에서는 가상함수가 동작하지 않음
#include <iostream> using namespace std; // 생성자에서는 가상함수가 동작하지 않는다. struct Base { Base() { goo(); } // void foo() { goo(); } virtual void goo() { cout << "Base::goo" << endl; } }; struct Derived : public Base { int x; Derived() : x(10) {} virtual void goo() { cout << "Derived::goo" << x << endl; } }; int main() { Derived d; // Base::goo }
-
생성자와 예외
- 생성자에서 예외가 발생 할 경우 소멸자가 호출되지 않음( 문제 : 메모리 누수 발생 가능 )
#include <iostream> using namespace std; struct Resource { Resource() { cout << "acquire Resource" << endl; } ~Resource() { cout << "release Resource" << endl; } }; class Test { Resource* p; public: Test() : p( new Resource ) { cout << "Test()" << endl; throw 1; } ~Test() { delete p; cout << "~Test()" << endl; } }; int main() { try { Test t; } catch (...) { cout << "예외 발생" << endl; } }
- 위 문제 해결 방법1 -> 스마트 포인터 사용
#include <iostream> #include <memory> using namespace std; struct Resource { Resource() { cout << "acquire Resource" << endl; } ~Resource() { cout << "release Resource" << endl; } }; class Test { // Resource* p; unique_ptr<Resource> p; public: Test() : p(new Resource) { cout << "Test()" << endl; throw 1; } ~Test() { //delete p; cout << "~Test()" << endl; } }; int main() { try { Test t; } catch (...) { cout << "예외 발생" << endl; } }
- 해결방법 2 -> 예외 가능성이 있는 어떠한 작업도 하지 않음, ex) 자원 할당 전용 함수 구현 장점 : 가상 함수 사용 가능 단점 : 초기화 의미의 함수를 두번 작성해야함
#include <iostream> #include <memory> using namespace std; struct Resource { Resource() { cout << "acquire Resource" << endl; } ~Resource() { cout << "release Resource" << endl; } }; // 해결책 2. two-phase constructor class Test { Resource* p; public: Test() : p(0) { // 예외 가능성이 있는 어떠한 작업도 하지 않는다. // 가상함수 호출() } // 자원 할당 전용함수 void Construct() { p = new Resource; //cout << "Test()" << endl; // 가상함수 호출() throw 1; } ~Test() { delete p; cout << "~Test()" << endl; } }; int main() { try { Test t; t.Construct(); // 필요한 자원 할당. } catch (...) { cout << "예외 발생" << endl; } }
Trivial
-
Trivial 개념
- 하는 일이 없는 경우
#include <iostream> #include <type_traits> using namespace std; class A { public: //virtual void foo() {} virtual 키워드를 붙일 경우 하는 일이 있는 것으로 봄 virtual void foo() {} }; int main() { cout << is_trivially_constructible<A>::value << endl; }
-
Trivial 활용
- 복사 생성자가 구현되있을 경우와 아닌 경우 처리를 다르게 하고 싶을 때 (객체 생성시 깊은복사, 얕은복사 선택 등)
#include <iostream> #include <type_traits> using namespace std; template<typename T> void copy_type(T* dst, T* arc, int sz) { if (is_trivially_copyable<T>::value) { cout << "복사 생성자가 trivial" << endl; memcpy(dst, src, sizeof(T)*sz); } else { cout << "복사 생성자가 trivial 하지 않은 경우" << endl; while (sz--) { new(dst) T(*src); ++dst, ++src; } } } int main() { char s1[20] = "hello world"; char s2[20] = { 0 }; copy_type(s1, s2, 20); // 모든 타입의 array를 복사하는 함수 }
-
Trivial로 판단되는 조건 정리
- virtual 함수가 아니어야 함
- 생성자가 없거나 구현되 있을 경우 default 키워드를 사용해야 함
- 객체형 멤버가 없거나, 객체 멤버의 생성자가 trivial 해야 함
- 상속 관계가 없거나, 부모 클래스의 생성자가 trivial 해야 함
- 멤버 변수 초기화 문법이 없어야함 ex) int a = 10;
Conversion
변환 연산자, 변환 생성자, explicit, nullptr 만들기 등
-
변환연산자 : 사용자 타입 -> 시스템 타입
#include <iostream> using namespace std; class Point { int x, y; public: Point() : x(0), y(0) {} Point(int a, int b) : x(a), y(b) {} // 변환 연산자 : 객체를 다른 타입으로 변환할때 호출된다. // 특징 : 리턴 타입을 표기하지 않는다. operator int() { return x; } }; int main() { int n = 3; double d = n; // 암시적 형변환 발생. Point p1(1, 2); n = p1; // 객체를 int에 대입할 때, 변환 연산자 필요 p1.operator int() cout << n << endl; // 1 }
-
변환 생성자 : 시스템 타입 -> 사용자 타입
#include <iostream> using namespace std; // Point => int : 변환 연산자 p.operator int() // int => Point : 변환 생성자 Point(int) class Point { int x, y; public: Point() : x(0), y(0) {} Point(int a, int b) : x(a), y(b) {} // 인자가 한개인 생성자 - 변환 생성자 // 다른 타입이 Point로 변환 되게 한다. Point(int a) : x(a), y(0) {} operator int() { return x; } }; int main() { Point p1; Point p2(1, 1); int n = 3; Point p(1, 2); n = p; // Point => int p.operator int() p = n; // int => Point n.operator Point() 가 있으면 된다. // 하지만, n은 사용자정의 타입이 아니다 }
-
변환의 장점
사용자 정의 타입을 기존 라이브러리의 필요 파라메터로 전달 할 수 있음
#include <iostream> using namespace std; // RAII : Resource Acquision Is Initialization class OFile { FILE* file; public: OFile(const char* filename, const char* mode = "wt") { file = fopen(filename, mode); } ~OFile() { fclose(file); } operator FILE*() { return file; } }; int main() { OFile f("a.txt"); // C 함수를 사용해서 파일 작업 fputs("hello", f); fprintf(f, "n = %d", 10); // OFile => FILE* 로 암시적 변환되면 가능. // f.operator FILE*() String s1 = "hello"; char s2[10]; strcpy(s2, s1); // String => const char* 암시적 변환.. }
-
변환의 단점
- 의도하지 않은 변환이 발생되어 버그가 발생 할 수 있음
#include <iostream> using namespace std; class OFile { FILE* file; public: // explicit 생성자 : 인자가 한개인 생성자가 암시적 변환을 // 허용하는 것을 막는다. explicit OFile(const char* filename, const char* mode = "wt") { file = fopen(filename, mode); } ~OFile() { fclose(file); } operator FILE*() { return file; } }; void foo(OFile f) {} int main() { OFile f("a.txt"); foo(f); // ok.. //foo("hello"); // const char* => OFile 로 암시적 변환 발생. // 변환 생성자 //foo(static_cast<OFile>("hello")); }
-
explicit : 암시적 형변환 관련 처리를 제한 시킬 수 있음
class Test { int value; public: // explicit Test(int n) : value(n) {} Test(int n) : value(n) {} }; int main() { // 아래 2줄의 차이점은 ? Test t1(5); // 인자가 한개인 생성자 호출 // direct initialization Test t2 = 5; // 1. 변환 생성자를 사용해서 5를 가지고 Test의 임시객체 생성 // 2. 임시객체를 복사 생성자를 사용해서 t2에 복사 // copy initialization // explicit 키워드를 사용하면 막을 수 있음 }
#include <string> #include <memory> using namespace std; class String16 { public: explicit String16(const char16_t* o); }; void foo(String16 s) // String16 s = "hello" {} int main() { foo("hello"); // error foo(String16("hello")); // ok.. String16 s = "hello"; // error // STL 의 string 클래스는 생성자가 explicit 가 아님. string s1("hello"); // ok string s2 = "hello"; // ok shared_ptr<int> p1 = new int; // error 생성자가 explicit shared_ptr<int> p2(new int); // ok.. }
-
nullptr 구현하기
- 0에 관해 살펴보기
int main() { int n1 = 10; // ok void* p1 = 10; // error. int n2 = 0; // ok void* p2 = 0; // ok. 0은 정수지만 포인터로 암시적 형변환된다. }
- 전통적인 NULL 정의
#include <iostream> using namespace std; void foo(int n) { cout << "int" << endl; } // 1 void foo(void* p) { cout << "void*" << endl; } // 2 void goo(char* p) { cout << "goo" << endl; } // 3 int main() { foo(0); // 1 foo((void*)0); // 2 #ifdef __cplusplus #define NULL 0 #else #define NULL (void*)0 #endif foo(NULL); // 2 goo(NULL); // void* => char* 로의 암시적 변환 필요. // C : ok // C++ : 암시적 변환 안됨. }
- 전통적인 c++에서의 문제점 : 정수 0은 있으나, 포인터 0이 없음
#include <iostream> using namespace std; void foo(int n) { cout << "int" << endl; } // 1 void foo(void* p) { cout << "void*" << endl; } // 2 void goo(char* p) { cout << "goo" << endl; } // 3 struct xnullptr_t { template<typename T> operator T*() { return 0; } }; xnullptr_t xnullptr; // 포인터 0 // C++ 11: xnullptr == nullptr int main() { foo(0); // 1 foo(xnullptr); // 2. xnullptr_t => void* 로의 암시적 변환 필요.. // xnullptr.operator void*() goo(xnullptr); // 3 goo int n = 0; double* p1 = xnullptr; double* p2 = nullptr; // C++11 의 포인터 0 nullptr_t a = nullptr; int* p = a; }
-
Return type resolver 좌변의 타입을 보고 우변의 리턴값을 결정 하는 방법
#include <iostream> #include <cstdlib> using namespace std; class memAlloc { int size; public: inline memAlloc(int sz) : size(sz) {} template<typename T> operator T*() { return (T*)malloc(size); } }; int main() { double* p1 = memAlloc(40); // 클래스이름() : 임시객체, 함수가 아님 // 임시객체.operator double*() int* p2 = memAlloc(40); }
-
Safe bool : 객체를 조건문에 넣을 때 operator bool()로 암시적 형변환이 시도됨
방법 1. bool 로 변환 - 단점. shift 연산이 허용된다.
operator bool() { return fail() ? false : true; }
방법 2. void* 로의 변환 - C++ 98/03
operator void*() { return fail() ? 0 : this; }
방법 3. 함수 포인터로의 변환.
typedef void(*F)(); operator F() { return fail() ? 0 : &true_function; }
방법 4. 멤버 함수 포인터로의 변환. -> Safe BOOL
void true_function() {} class istream // basic_istream { public: bool fail() { return false; } // 방법 1. bool 로 변환 - 단점. shift 연산이 허용된다. //operator bool() { return fail() ? false : true; } // 방법 2. void* 로의 변환 - C++ 98/03 // operator void*() { return fail() ? 0 : this; } // 방법 3. 함수 포인터로의 변환. //typedef void(*F)(); //operator F() { return fail() ? 0 : &true_function; } // 방법 4. 멤버 함수 포인터로의 변환. - Safe BOOL // if() 문에 넣을수 있는 side effect가 가장 적다.. struct Dummy { void true_function() {} }; typedef void(Dummy::*F)(); operator F() { return fail() ? 0 : &Dummy::true_function; } }; istream cin; int main() { int n = 0; if ( cin ) {} // cin << n; // delete cin; // void(*f)() = cin; }
-
Explicit Conversion Operator (safe bool을 간결하게 구현 가능)
class istream { public: bool fail() { return false; } // C++11 부터는 변환 연산자 앞에도 explicit를 붙일수 있다. // 암시적 변환은 error. 명시적 허용 // if 문안에 객체를 넣을수도 있다. explicit operator bool() { return fail() ? false : true; } }; istream cin; int main() { int n = 0; //bool b = cin; //error bool b = static_cast<bool > (cin); // ok //cin << n; // error if ( cin ) {} // ok if ( cin == false ) {}// error }
-
Brace-init(일관된 초기화)과 변환
class Point { int x, y; public: explicit Point(int a, int b) : x(a), y(b) {} }; void foo(Point p) {} int main() { foo({ 1,1 }); // error, explicit으로 인해 // Point p1(1, 1); // Point p2{ 1, 1 }; // direct initialize (직접 초기화) // Point p3 = { 1, 1 };// copy initialize. error (변환 생성 사용, explicit로 인해 error) }