음악, 삶, 개발

14. Graphics Class Design 본문

개발 공부/Principles And Practice Using C++

14. Graphics Class Design

Lee_____ 2020. 7. 21. 05:02

interface 디자인의 아이디어와, inheritance (상속) 의 개념을 설명한다.

또한 class derivation (클래스 파생), virtual function (가상 함수), access control 을 다룬다.

 

< 목차 >

 

14.1 Design principles

14.1.1 Types

14.1.2 Operations

14.1.3 Naming

14.1.4 Mutability (가변성)

14.2 Shape

14.2.1 An abstract class

14.2.2 Access control

14.2.3 Drawing shapes

14.2.4 Copying and mutability

14.3 Base and derived classes

14.3.1 Object layout

14.3.2 Deriving classes and defining virtual functions

14.3.3 Overriding

14.3.4 Access

14.3.5 Pure virtual functions

14.4 Benefits of object-oriented programming


14.1 Design principles

14.1.1 Types

 

생략.


14.1.2 Operations

 

함수를 만들때 좋은 디자인과 나쁜 디자인 있다.

예를 보자.

// 좋은 디자인

void drawRectangle( Point p, int width, int height); 

drawRectangle(Point {100, 200}, 300, 400); // 코드를 보았을때, argument 를 보고 예측 가능.

// 나쁜 디자인
void drawRectangle(int x, int y, int width, int height);

drawRectangle(100, 200, 300, 400); // argument 를 보고 예측 불가.

class 를 디자인할때 언제나 디자인적인 지속성, 일반성 (genertic) 을 유지하라.

예를 보자.

 

class Rectangle {

	void drawRect();

}

class Line {
	
    void drawLine();

}

14.1.3 Naming

 

프로그래밍에서 add 와 attach 라는 단어를 자주 볼것이다.

차이는 이렇다.

  • add : pass-by-value (복사)
  • attach : pass-by-reference (단일 객체를 공유)

attach 된 객체는 사용되는 동안 언제나 valid (유효하거나) 또는 survice (생존) 해야한다.

아래 예를 보자.

void f(Simple_window& w) {
	
    Rectangle r {Point {100, 200}, 50, 30};
    
    w.attach(r);
    
    // r 은 이 함수가 끝나면 사라짐. (lifetime's r end here)

}

int main() {

	Simple_window win { Point {100, 100}, 600, 400, "My window" };

	f(win);
    
    win.wait_for_button(); // 

}

14.1.4 Mutability (가변성)

 

우리가 class 를 디자인할때 스스로에게 이 질문들을 꼭 하라.

  1. 누가 data (representation) 를 modify 하는가?
  2. 어떻게 data 를 modify하는가?
  3. 이 class 의 객체가 생성되었을때, 그 객체는 수정할수있는지 없는지?
  4. class 바깥의 함수 (member 함수가 아닌 함수) 가 이 class 의 객체의 값을 읽을 필요가있는지 없는지?

우리는 언제나, class 만이 객체의 상태를 수정할수있도록 해야한다.

이때, public / protected / private 이 등장한다.

 

private 에 대한 간단한 예.

struct Circle {

	private :
    
    	int r; // radius

};

Circle c {Point{100, 200}, 50};

c.r = -9; // error : Circle::r is private

위의 코드처럼, 우리는 data 멤버들에 대한 direct access 를 막아야한다. 

direct access 가 가능하다고 하면 위의 코드의 radius 값 -9 가 말이 되는가?

어떻게 이걸 막을것인가?

(결국 data member 를 private 으로 보내고, set 함수로 바꾸는것은 bad input 을 막기위한 필수적인 방어책이기도 하다)


14.2 Shape

 

Shape class 를 만들어보자.

class Shape {

    public :

        void draw() const;
        virtual void move(int dx, int dy); 

        void setColor(Color col);
        Color getColor() const;

        void setFillColor(Color col);
        Color fillColor() const;

        Point getPoint(int i) const;
        int getNumberOfPoints() const;

        Shape(const Shape&) = delete; // prevent copying
        Shape& operator = (const Shape&) = delete;


        virtual ~Shape() {}

    protected :

        Shape() {}
        Shape(std::initializer_list<Point>list);

        virtual void drawLines() const;
        void add(Point p);
        void setPoint(int i, Point p);


    private :

        vector<Point> points;
        Color lineColor { fl_color() };
        Line_style lineStyle { 0 };
        Color fColor { Color:: invisible };

};

14.2.1 An abstract class

 

protected : 파생 class 에서만 접근할수있음.

위의 코드에는 constructor 가 protected 안에 들어가있다.

protected :
	
    Shape() { }

2가지의 중요한 목적이 있다.

 

  • 이 class 는 오직 파생 class 를 위한 base class 로만 사용될수있다.
  • 우리는 이 클래스의 객체를 직접 만들수없다. (directly)

protected 에 constructor 가 있는 경우, 객체를 단독으로 생성할수없다.

이렇게 protected 에 constructor 를 집어넣는것은, 

또다른 중요한 의미를 지닌다.

Shape 를 객체를 만드는것이 의미적으로도 말이 안된다는것을 코드에서 express 하는것이다.

우리는 Shape 을 가질수없고, Circle 이나 Rectangle (from Shape) 을 가질수있는것이다.

"무슨 shape인가? " 라고 묻지, Shape 자체가 나타낼수있는것은 추상적이다 (abstract).

shape 는 하나의 개념인것이다. ex) Midi controller 또한 하나의 개념이다.

이러한 concept 을 코드상으로 표현하는 class 를 만들때는 위와 같이

protected 에 constructor 를 넣는것이 굉장히 중요한 design 테크닉이다.

이렇게 base class 로만 사용할수있는 class 를 abstract class 라고한다.

abstract class 를 만드는 가장 일반적인 방법은 pure virtual function (순수 가상함수) 를

사용하는것이다. (14.3.5 에서 자세히 설명함)

abstract class 의 반대말은 concrete class 이다.

정리하면.

 

  • abstract class : 파생 class 를 위핸 base class 로만 쓰일수있는 class (단독 객체 생성 x)
  • concrete class : 객체를 만들기위해 사용되는 class

우리가 camera 가게에 가서, camera 를 달라고할수있는가?

Sony A7s2 (내가 가지고있는 카메라 모델명) 를 주세요라고 해야되지않나.

단어 "camera" 는 일반화 (generalization) 이자 abstract 이다.

코드로 간단히 표현해보면..

Camera 는 abtract class 이다.

SonyA7s2 는 concrete class (derived class) 이다.

sonyA7s2 는 내 손에 들려있는 진짜 카메라 object 이다.

 

constructor 의 위치에 관한 이야기를 했으니,

이 부분이 궁금하다..

destructor 말이다.

virual ~Shape() {} 

virtual destructor 에 대해 17.5.2 에서 다룰것이다.

 

 

abstract class 안에서 protected 안에 함수가 들어가 있는 이유는,

파생 class 가 전용으로 사용할 함수라는것이다.

abstract class 를 만드는것 자체가 결국  파생 class 를 위한것임을 잊지말자.

abstract class 의 constructor 는 protected 안에 있다!

 

이 부분은 매우 중요하다. 자주와서 읽어보자.


14.2.2 Access control

 

constructor 의 arguments 로 아무것도 오지않는다는것은

constrctur 가 data member 를 초기화하지않는다는것이다.

이럴때는 in-class (class 내에서) 초기화를 하도록 한다.

class Hello {
	
    public : 
    
    	Hello() {} // argument 가 없음.
    
    private :
    
    	vector<int> v; // empty vector 로 초기화
    	int something { 1 };


}

 

data member 가 private 에 있을때,

우리는 이 data 를 read & write 할수있는 함수를 만들어야한다.

아래가 best practice 다.

class Lee {

    public :

        // for Write (쓰기)
        void setYear(int newYear) {

            year = newYear;

        }
        
        // for Read (읽기)
        // get 함수뒤에는 const 를 붙여주는것이 일반적이다.
        // 우리는 get 할뿐이지, 내부에서 먼가를 수정하지않을것이기때문에.
        int getYear() const {

            return year;

        }

    private :

        int year { 1985 };

};

14.2.3 Drawing shapes

 

생략.


14.2.4 Copying and mutability

 

C++ 에서는 copy(복사)를 방지하는 기능이 있다.

예를 보자.

 

= delete keyword 를 뒤에 붙임으로써, default operator 를 삭제할수있다.


14.3 Base and derived classes

 

class 을 design 할때 우리는 3가지 주요한 메카니즘을 사용한다.

 

  • Derivation (파생) : inheritance (상속) 으로 부르기도한다. 파생 class 는 sub class, base class 는 super class.
  • Virtual Function (가상함수) : run-time polymorphism 이라고도 부른다. run-time 에 객체에 따라 함수가 결정되므로.
  • Private and Protected members : encapsulation (은닉화) 라고도 부른다. direct access 로부터 보호한다.

따라서 object oriented programming (객체 지향 프로그래밍) 의 중요한 3가지는 다음과 같다.

  1. inheritance (상속)
  2. run-time polymorphism (런타임 다형성)
  3. encapsulation (은닉화)

inheritance relationships

화살표의 끝이 있는 방향이 base class 이다.

자식들이 부모에게 손을 뻗는 모습이라고 생각하면 이해가 쉽다.

위와 같은 그림을, class hierarchy (클래스 계층구조) 라고도 부른다.

위의 그림에서 Shape 는 abstract class 로써 abstract concept 를 대표한다.


14.3.1 Object layout

 

class 의 객체가 memory 에 어떠한 모습으로 저장되는가에 대한 이야기이다.

virtual function table, virtual pointer 등의 내용이 나온다.

굳이 몰라도 되는 내용이지만, 내부 구현이 궁금한 사람들을 위해 서술 해놓았다고 한다.

(virtual function 이 performance 에서 expenssive 하다는 미신이 있다면서)

추후 검색을 통해 깊이 알아보도록 한다.


14.3.2 Deriving classes and defining virtual functions

 

virtual 을 함수의 가장 앞에 붙힌다.

class 내부에서 선언후 바깥에서 정의할때는 virtual 을 쓸 필요없고, 쓰면 error 난다.

(virtual 은 class 바깥에서 쓸수없는 keyword 임, 빨간줄 그어짐)

virutal 함수의 선언과 바깥에서 정의.


14.3.3 Overriding

 

override 를 설명하기앞서, 알아야할 중요한 개념이 있다.

Base 클래스의 객체를 argument 로 받는 함수를 만들었다고 했을때,

파생 클래스, 파생 클래스의 파생 클래스, 파생 클래스의 파생 클래스의 파생 클래스....의 객체도

똑같이 Base 클래스의 객체로 간주되어 함수의 argument 로 넘길수있다.

 

자식 class 의 객체도 base 클래스를 받는 함수에 넘길수있음.

 

virtual function 을 override (덮어쓰기) 할떄는, base class 에서 정의된 함수의 name 과 type 이

정확하게 일치하여야한다. (함수 뒤에 붙는 const 까지 똑같이 써줘야함)

이것들이 일치하지않는 함수는 base class 의 함수를 override 하는것이 아닌,

새로운 함수라고 간주한다.

override keyword 를 함수끝에 붙임으로써,

내가 작성하고있는 함수의 정의가, base class 의 virtual 함수를 정의하고있음을 compiler 에게 알려줄수있다.

아래의 예는 base 의 클래스의 파생 클래스들이 만들어졌을때,

어떻게 함수가 호출되는지 알려주는 아주 좋은 예이다.

추후 Juce 를 하다가 헤깔릴때마다, 이 예제를 통해 개념을 되집어 보도록하자.

#include <iostream>



struct A {

    virtual void f() const  { std::cout << "A 의 f" << std::endl; }
    void g() const          { std::cout << "A 의 g" << std::endl; }

};

struct B : A {

    void f() const override { std::cout << "B 의 f" << std::endl; } // B 의 f 를 override
    void g() const          { std::cout << "B 의 g" << std::endl; }

};

struct C : B {

    void f()                { std::cout << "C 의 f" << std::endl; } // const 가 뒤에없으므로 override 아님.
    void g() const          { std::cout << "C 의 g" << std::endl; }

};

void call(const A& a) {

    a.f();
    a.g();

}



void main() {

    A a;    
    call(a);    // A의 f, A 의 g

    B b;
    call(b);    // B 의 f, A 의 g
 
    C c;
    call(c);    // B 의 f, A 의 g
                // C 에서 a 의 f 가 override 되어있지않으므로 타고올라가
                // b 에 있는지 확인하는데, b.f() 는 override 이므로 b.f() 를 호출.

    // a.g() 는 계속해서 호출 되고있다.
    // 왜냐면, a.g() 는 자식들중에 누구도 override 하고있지않음.

}



 


14.3.4 Access

 

  • Pulic: 어떤 함수든 member 를 접근할수있음.
  • Protected : 자기 자신, 파생 class 의 member 에 의해 접근 가능.
  • Private: member 그 priave 이라면, class 안의 다른 member 에 의해서만 접근 가능.

접근 관계

friend 다른 개념이 있긴한데, 너무 방대하다고 함. 나중에 따로 찾아볼것.

 

Bjarne Stroustrup 의 말

 

당신이 C++ 의 모든 기능을 다 알고있는 변호사같은 사람이 되고싶다면 

아래의 책을 읽어라.

  • The Design and Evolution of C++
  • The C++ Programming Lanuage
  • ISO C++ standard

하지만, 난 당신이 변호사가 되는것을 추천하지않는다.

변호사가 아닌 프로그래머가 되는것이 훨씬 즐겁고, 이 사회에 도움이 되는 일이다.


14.3.5 Pure virtual functions (개중요!!!!!!!!!)

 

너무 너무 중요한 부분이다.

혹시 내가 모호하게 알고있다고 생각하면, 추후 구글링을 통해 보충해야한다.

 

abstract class : 오직 base class 로써만 쓰일수있는 class

프로그래밍에서 abstract class 는, 연관된 class 를 묶어주는 interface 역할을 한다. (매우 중요한 설명!)

(실제 현실에서는, animal, book 등을 abstract class 라고 생각할수있다.)

 

14.2.1 에서 우리는 protected 에 constructor 를 집어넣어 abstract class 를 만드는 방법을 배웠다.

하지만 훨씬 더 많이 쓰이는 방식이 있다. 

파생된 클래스에 의해 반드시 override 되어야만 하는 함수 pure virutal function 을 만드는것이다.

pure virtual class 를 가지고있는 함수를 1개라도 가지고있는 함수는 abstract class 가 되고,

인스턴스를 생성하려하면 error 가 난다. = 0; 을 함수뒤에 붙여 만든다.

abstract class 를 pure virtual function 을 통해 만들기

Base 클래스에서 정의한 pure virtual 중 하나라도, 파생 클래스에서 정의되지않으면

파생 클라스 또한 여전히 abstract class 로 간주되어 객체를 생성할수없다.

 

g() 를 Child 가 정의하지않아, 여전히 abstract class 이므로 객체 를 생성할수없다는 error

 

더 하위 class 에서, Base 클래스의 함수를 마저 정의해준다면,

object 로 만들수있다. 

 

GrandChild 에서 g() 를 정의함으로써, main() 안에서 객체를 생성했다.

pure virtual 함수를 가진 class 는 pure interface 로 사용되는 경향이 크다.

따라서, abstract class 는 data member 를 가지지않는것이 일반적이다.

(data member 는 파생 클래스가 가진다.)

abstract class 가 data member 를 가지고있지않다는것의 의미는,

member 를 위한 초기화가 필요없다, 즉 constructor 가 필요없다는것을 의미한다.

 

data member 가 없는 class 는 constructor 가 필요없다.

마지막으로 정리해본다.

 

abstract class 란?

 

  • 객체로 생성할수없다 (인스턴스화).
  • pure virtual function 을 가진 class 이다.
  • pure virtual function 은 함수 뒤에 = 0 을 붙여서 만든다.
  • 자식이 한번에 abstract class 의 모든 virtual 을 override 할 필요는 없다. (자식의 자식이 하면 된다)
  • 하지만 자식 class 가 override 하지않으면 자식 class 역시 abstract class 가 되어 객체를 생성할수없다.
  • pure interface 로 쓰이는것이 일반적이다.
  • 따라서 data member 를 갖지않고, 따라서 constructor 를 갖지않는다.

14.4 Benefits of object-oriented programming

 

우리가 class 의 상속을 공부하면서 알아야할 중요한 2가지 있다.

 

  • Interface inheritance : 부모 class 를 인자로 받는 함수 (주로 reference 로) 는 자식 class 역시 인자로 받을수있다.
  • 또한, 자식 class 는 부모가 class 가 제공한 interface 를 사용할수있다.
  • Implementation inheritance : 자식 class 를 define 할때, 부모가 class 가 제공하는 data, member 함수를 사용할수있다.

interface 상속을 사용하지않은 디자인은 매우 bad practice 이다.

또한 함수를 override 할때 매우 중요한 점은, 

원래의 함수가 의미하는것을 이어가는쪽으로 재정의하라는것이다.

 

class 의 상속을 설명할때 interface 와 implementation 의 의미는 아래와 같다.

  • Base (Abstract) class : interface
  • Derived Class (파생 class) : implementation
// Interface
class Base {

    public :

        virtual void f() = 0; // pure virtual
        virtual void g() = 0;

};

// Implementation
class Child : public Base {

    public :

        void f() override {}
        void g() override {}

};

 

class 의 상속을 활용하는것은, code 를 고치지않고 무언가를 추가할수있는

프로그래밍의 아주 중요한 테크닉이다.

유용한 것들을 Base class 에 정의해놓음으로써, 

우리는 파생 class 에서 지루한 반복적인 작업을 반복할 필요가 없는것이다.


Review

 

  1. What is an application domain?
  2. What are ideals for naming?
  3. What can we name?
  4. What services does a Shape offer?
  5. How does an abstract class differ from a class that is not abstract?
  6. How can you make a class abstract?
  7. What is controlled by access control?
  8. What good can it do to make a data member private?
  9. What is a virtual function and how does it differ from a non-virtual function?
  10. What is a base class?
  11. What makes a class derived?
  12. What do we mean by object layout?
  13. What can you do to make a class easier to test?
  14. What is an inheritance diagram?
  15. What is the difference between a protected member and a private one?
  16. What member of a class can be accessed from a class derived from it?
  17. How does a pure virtual function differ from other virtual functions?
  18. Why would you make a member function virtual?
  19. Why would you make a virtual member function pure?
  20. Why does overriding mean?
  21. How does interface inheritance differ from implementation inheritance?
  22. What is object-oriented programming?

Terms

 

  • abstract class
  • access control
  • base class
  • derived class
  • dispatch
  • encapsulation
  • inheritance
  • mutability
  • object layout
  • object-oriented
  • override
  • polymorphism
  • private
  • protected
  • public
  • pure virtual function
  • sub class
  • super class
  • virtual function
  • virtual function call
  • virtual function table