I'm FanJae.

[C++] Operator Overloading III 본문

C++/Basic

[C++] Operator Overloading III

FanJae 2024. 8. 25. 17:35

※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.

 

1. std::cout의 원리

#include <iostream>
int main()
{
    std::cout << 10;
}

 

1-1. std::cout의 원리

- std::ostream 타입의 객체이다.

- operator<< 연산자 재정의 기술을 사용하여 재정의 한 것이다.

#include <iostream>

/*
namespace std
{
     class ostream
     {
     };
     
     ostream cout;
}*/

int main()
{
    std::cout << 10; // cout.operator<<(10)
    
    std::cout.operator<<(10);
    
 }

- 즉, 실제로는 std namespace안에 ostream이라는 class가 존재한다는 것이다.

- 또한 std::cout은 std::cout.operator<<(10); 과 같이 호출이 가능하다.

 

※ 정확히 말하면, basic_ostream 이라는 클래스 템플릿이다.

template< typename CharT, typename Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<charT, Traits>

using ostream = basic_ostream<char, char_traits<char>>;

- 이와 같이 처리되어 있는 것이다.

 

1-2. std::ostream과 유사한 클래스 만들기

- 연산자 재정의 문법을 이해 하기 위해서 사용하는 것으로 화면 출력은 printf를 사용할 것임.

- 실제 basic_ostream의 구현은 출력 버퍼 등을 사용해 상당히 복잡하다고 한다.

#include <stdio.h>

namespace std // 학습 용도
{
    class ostream
    {
    
    };
    ostream cout;
}
int main()
{
    std::cout << 10;
}

- 이 예제는 학습의 용도일 뿐이다. 절대 std에 임의로 뭔가 추가해서 쓰면 안된다.

 

1-2-1. operator<<() 연산자 함수 제공

#include <stdio.h>

namespace std
{
    class ostream
    {
    public:
    	void operator<<(int         arg) { printf("%d",arg); }
        void operator<<(char        arg) { printf("%c",arg); }
        void operator<<(const char* arg) { printf("%s",arg); }
        void operator<<(void*       arg) { printf("%p",arg); }
    };
    ostream cout;
}
int main()
{
    std::cout << 10;
}

- 모든 표준 타입에 대해서 제공이 가능해야 한다.

- 한편 cout은 한번만 출력하지 않는다. 아래 처럼 여러번 출력도 가능하게 해야한다.

std::cout << 10 << '\t' << 20 << '\n';

- 우린 이러한 상황을 여러번 시도한적이 있다.

 

1-2-2. << 을 사용한 연속적인 출력

#include <stdio.h>

namespace std
{
    class ostream
    {
    public:
    	ostream& operator<<(int         arg) { printf("%d",arg); return *this; }
        ostream& operator<<(char        arg) { printf("%c",arg); return *this; }
        ostream& operator<<(const char* arg) { printf("%s",arg); return *this; }
        ostream& operator<<(void*       arg) { printf("%p",arg); return *this; }
    };
    ostream cout;
}
int main()
{
    std::cout << 10 << '\t' << 20 << '\n';
}

- 이와 같이 operator<<() 함수가 자기 자신을 참조로 반환하도록 해야한다.

- 또한, operator<<() 함수가 non-const member function임에 유의해야 한다.

 

1-2-3. std::cout을 상수 참조로 가리키면 발생하는 문제점

#include <iostream>

void f1(std::ostream& os)
{
     os << "hello\n";
}

void f2(const std::ostream& os)
{
     os << "hello\n"; // os.operator<<(...);
}

int main()
{
    f1(std::cout);
    f2(std::cout);
}

- const를 받지 못하는 이유는 함수 내부적으로 출력 버퍼 등의 상태를 변경하기 때문에, const 값을 받을 수 없다.

- 따라서, std::cout을 상수 참조로 가리키면 operator<<() 함수 사용이 불가능하다.

- std::cout을 함수 인자로 받을 때는 non-const reference를 사용해야한다.


2.std::endl의 정체

#include <stdio.h>

namespace std
{
    class ostream
    {
    public:
    	ostream& operator<<(int         arg) { printf("%d",arg); return *this; }
        ostream& operator<<(char        arg) { printf("%c",arg); return *this; }
        ostream& operator<<(const char* arg) { printf("%s",arg); return *this; }
        ostream& operator<<(void*       arg) { printf("%p",arg); return *this; }
    };
    ostream cout;
}

int main()
{
    std::endl ( std::cout );
    std::cout << std::endl; // 위 2개다 문제되지 않는다.
}

- std::endl는 함수이다.

- 따라서, 위처럼 인자값을 받는 행위가 아무런 문제가 되지 않는다. 

- 이것 역시, std namespace에 구현될 것이기 아래와 같이 추가할 수 있다.

 

2-1. std::endl의 구현

namespace std
{
    class ostream
    {
    public:
    	ostream& operator<<(int         arg) { printf("%d",arg); return *this; }
        ostream& operator<<(char        arg) { printf("%c",arg); return *this; }
        ostream& operator<<(const char* arg) { printf("%s",arg); return *this; }
        ostream& operator<<(void*       arg) { printf("%p",arg); return *this; }
    };
    ostream cout;
    
    ostream& endl(ostream& os)
    {
          os << '\n';
          return os;
    }
}

int main()
{
    std::endl ( std::cout );
    // std::cout << std::endl; 
}

- 구현하면, std::cout << std::endl;은 아직 작동하지 않는다.

- 하지만 이것 역시도 구현이 가능하다.

- std::endl;은 함수다. 즉, 이것은 cout.operator<<(endl); endl의 주소 값을 받아오면 된다. (함수 포인터로 받아서)

 

2-2. 함수 포인터를 추가한 std::endl의 구현

namespace std
{
    class ostream
    {
    public:
    	ostream& operator<<(int         arg) { printf("%d",arg); return *this; }
        ostream& operator<<(char        arg) { printf("%c",arg); return *this; }
        ostream& operator<<(const char* arg) { printf("%s",arg); return *this; }
        ostream& operator<<(void*       arg) { printf("%p",arg); return *this; }
    };
    ostream cout;
    
    ostream& endl(ostream& os)
    {
          os << '\n';
          return os;
    }
    ostream& operator<<(ostream&)(*f)(ostream&))
    {
     	f(*this); // endl(cout)
       return *this;
    }
}

int main()
{
    std::endl ( std::cout );
    // std::cout << std::endl; 
}

operator<<(함수 포인터) 로 전달 받아서 다시 endl 함수를 호출하는 원리이다.

- std::endl, std::hex, std::dec 등도 모두 함수이다.

- 이들을 보통 입출력 조정자 함수라고 칭하는데 사용자가 새로운 조정자 함수를 추가하는 것도 가능하다.

namespace std
{
    class ostream
    {
    public:
    	ostream& operator<<(int         arg) { printf("%d",arg); return *this; }
        ostream& operator<<(char        arg) { printf("%c",arg); return *this; }
        ostream& operator<<(const char* arg) { printf("%s",arg); return *this; }
        ostream& operator<<(void*       arg) { printf("%p",arg); return *this; }
    };
    ostream cout;
    
    ostream& endl(ostream& os)
    {
          os << '\n';
          return os;
    }
    ostream& operator<<(ostream&)(*f)(ostream&))
    {
     	f(*this); // endl(cout) // tab(cout)
       return *this;
    }
}

std::ostream& tab(std::ostream& os)
{
     os << '\t';
     return os;
}
std::ostream& menu(std::ostream& os)
{
     os << "1. AAA\n";
     os << "2. BBB\n";
     return 0s;
}

int main()
{
    std::endl ( std::cout );
    std::cout << std::endl; // cout.operator<<(endl);
    
    std::cout << "A" << tab << "B" << std::endl; 
    
    std::cout << menu;
}

- 이와 같이 원리를 이해하고 있다면, tab, menu와 같은 것도 추가할 수 있다.

- 성능의 경우는 이 때문에 std::endl을 사용하는 것 보다 '\n'을 사용하는 것이 빠르다.

 

2-3. 결합도 가능한가?

#include <iostream>

std::ostream& tab(std::ostream& os)
{
     os << '\t';
     return os;
}
std::ostream& menu(std::ostream& os)
{
     os << "1. AAA\n";
     os << "2. BBB\n";
     return 0s;
}

int main()
{ 
    std::cout << menu; // 표준 C++ Cout과 우리가 만든 메뉴를 섞어쓰고 있다.
}

- 표준과도 잘 연동되는 것을 확인할 수 있다.

- std::cout이 성능이 떨어진다고 하는 이유가 이러한 디자인 때문도 있다.


3. user define type & std::cout

#include <iostream>
class Point
{
    int x{0};
    int y{0};
public:
    Point(int x, int y) : x{x}, y{y} {}
};

int main()
{
    Point p{1,2};
    std::cout << p; // error.
    
    std::cout << 1; // cout.operator<<(int)
    std::cout << 3.4; // cout.operator<<(double)
}

- std::cout은 Point를 어떻게 출력해야할지 몰라서 오류가 발생한다.

- 다만, cout.operator<<(Point) 처럼 멤버로 추가는 불가능하다.

 

std::cout으로 사용자 정의 타입(Point)의 객체를 출력하려면, operator<<(std::ostream, Point) 함수 제공이 필요하다.

 

3-1. 사용자 정의 타입의 출력

std::ostream& operator<<(std::ostream& os, const Point& pt)
{
     return os << pt.x << ", " << pt.y << std::endl;
}

- 이와 같이 출력처리 하면, 사용자 정의 타입의 객체도 출력할 수 있다.

#include <iostream>
class Point
{
    int x{0};
    int y{0};
public:
    Point(int x, int y) : x{x}, y{y} {}
    
    friend std::ostream& operator<<(std::ostream& os, const Point& pt);
};

std::ostream& operator<<(std::ostream& os, const Point& pt)
{
     return os << pt.x << ", " << pt.y << std::endl;
}

int main()
{
    Point p{1,2};
    std::cout << p;
    
    std::cout << 1; // cout.operator<<(int)
    std::cout << 3.4; // cout.operator<<(double)
}

- 이와 같이 구현하면, std::cout에서도 사용자 정의 객체를 잘 출력해준다.

- private인 멤버에 접근 하기 위해서는 getter또는 friend 함수로 만들어 주면 된다.

 

3-1-1. 구현된 내용 추가 설명.

std::ostream& operator<<(std::ostream& os, const Point& pt)
{
     return os << pt.x << ", " << pt.y << std::endl;
}

- 위 문장에서 std::ostream& os는 const가 아님에 반해, const Point& pt는 const이다.

 

Point& pt에는 const를 붙이는 이유

const Point p{1,2};
std::cout p; // 상수 객체도 출력은 가능해야한다.

- 아무리 상수 객체라도 출력은 가능해야 한다.

- 상수 객체를 받아주기 위해서는 인자 값을 const로 받아야 한다.

 

std::ostream& os가 const가 없는 이유

- 이 내용은 앞서 다뤘듯 operator<< 는 non-const member function이다.

- 실제 std::ostream에 구현되어 있는 operator<< 는 출력 버퍼등의 상태가 변환된다. 

- 따라서, const를 붙이면 출력 자체가 불가능해진다.

 

3-2. User define type(Point)가 특정 이름 공간 안에 있는 경우

#include <iostream>

namespace Graphics
{
    class Point
    {
        int x{0};
        int y{0};
    public:
        Point(int x, int y) : x{x}, y{y} {}

        friend std::ostream& operator<<(std::ostream& os, const Point& pt);
    };
    
    std::ostream& operator<<(std::ostream& os, const Point& pt)
    {
        return os << pt.x << ", " << pt.y << std::endl;
    }
}
int main()
{
    Graphics::Point p{1,2};
    std::cout << p; // ? // operator<<(cout, p)
}

- std::cout 입장에서는 Graphics라는 정보가 전혀 없지만, 이 경우에도 실행되는데 전혀 문제가 없다.

- 이것이 가능한 이유는 'ADL' 이라는 것 때문에 가능하다고 한다.

- ADL에 대해서는 별도로 포스트를 다뤄보고자 한다.

 

 

 

 

'C++ > Basic' 카테고리의 다른 글

[C++] STL Container I  (0) 2024.08.27
[C++] Operator Overloading IV  (2) 2024.08.26
[C++] Operator Overloading II  (0) 2024.08.24
[C++] Operator Overloading I  (0) 2024.08.23
[C++] Multiple Inheritance, Diamond Inheritance, Virtual Inheritance  (0) 2024.08.23
Comments