Eat Study Love

먹고 공부하고 사랑하라

SW 만학도/C++ & Algorithm

VS code C++ Debugging & Operator overloading

eatplaylove 2025. 2. 3. 15:27

이번엔, C++ 코딩을 하며 자주 마주하는 디버깅의 현장을 살펴보겠다.

 

관련자료는 아래와 같다.

 

13_Debugging and Exercises.pdf
10.02MB
13_debugging_prerequisite.pdf
1.55MB
13_exercise_2.h
0.01MB
13_exercise_2_ans.h
0.01MB
13_test_cases_2.cpp
0.00MB
list_pop.cpp
0.00MB
operator_overloading.cpp
0.00MB
simple_vector_bug.cpp
0.00MB

 

용량이 크진 않으니 VS Code를 통해 C++ 코딩 작업을 할 경우, 잘 참고해서 하면 되겠다.

 

기본적으로 디버깅할땐 print문을 통해서 디버깅하는 방법이 있고, VS Code 상의 Debug Extention을 이용하는 방법이 있다. 전자는 직관적이지만, 귀찮고 후자는 효과적이지만 방법을 터득하기가 귀찮다.

 

일단 간단하게 Debugging 실습을 진행해보자.

 

#include <list>
#include <iostream>

using namespace std;

int main(){
    list<int> lst = {1,2,3,4,5};

    // i want to practice using pop_back function in list!
    for(int v : lst){
        lst.pop_back();
        cout << "popped" << endl;
    }
    return 0;
}

 

위 코드를 cout으로 디버깅하려고 하면, 코드가 실행되긴 하는데 Error(빨간 불)이 뜬다.

 

근데 그 원인이 뭔진 알 수 없는 것이다.

 

Debug tool을 써야 어디가 ERROR인 지 알고, 비로소 고칠 수 있는 것

#include <list>
#include <iostream>

using namespace std;

int main(){
    list<int> lst = {1,2,3,4,5};

    // i want to practice using pop_back function in list!
    for(int v : lst){
        lst.pop_back();
        cout << "popped" << endl;
    }
    return 0;
}

 

 

두 번째, Operator_overloading 디버그 예제는 아래와 같다.

Break Point 외에 Step-into 로 단계별 Variable의 변화모습을 체크할 수도 있다.

 

그리고 마지막으로 Simple_vector를 이용해서 Debugging 해보기

 

Simple_vector의 경우도 Compiler에서 안 잡아주는 Error Point를 Debugging tool은 잡아준다.

#include <iostream>
#include <memory>
#include <initializer_list>

template <typename T>
class SimpleVector {
private:
    T* array;
    int size;
    int capacity;

    void resize();

public:
    SimpleVector(int initialCapacity);
    SimpleVector(std::initializer_list<T> elements);
    ~SimpleVector();

    void addElement(T element);
    int getSize() const;
    
    T& operator[](int index) const;
};

template <typename T>
SimpleVector<T>::SimpleVector(int initialCapacity) : size(0), capacity(initialCapacity) {
    array = new T[capacity];
}

template <typename T>
SimpleVector<T>::SimpleVector(std::initializer_list<T> elements)
: size(0), capacity(elements.size()) {
    // cout << "Initializer list constructor called" << endl;
    array = new T[capacity];
    for (auto element : elements) {
        addElement(element);
    }
}

template <typename T>
SimpleVector<T>::~SimpleVector() {
    delete[] array;
}

template <typename T>
void SimpleVector<T>::addElement(T element) {
    if (size == capacity)
        resize();
    array[size] = element;
    size++;
}

template <typename T>
int SimpleVector<T>::getSize() const {
    return size;
}

template <typename T>
void SimpleVector<T>::resize() {
    capacity = capacity * 2;
    T* newArray = new T[capacity];
    for (int i = 0; i < size; i++)
        newArray[i] = array[i];
    delete[] array;
    array = newArray;
}

template <typename T>
T& SimpleVector<T>::operator[](int index) const {
    return array[index];
}

int main() {
    SimpleVector<int> vec1 = {1, 2, 3};
    SimpleVector<int> vec2 = vec1;

    vec1.addElement(4);
    vec2.addElement(5);

    std::cout << "vec1: ";
    for (int i = 0; i < vec1.getSize(); i++) {
        std::cout << vec1[i] << " ";
    }
    std::cout << std::endl;

    std::cout << "vec2: ";
    for (int i = 0; i < vec2.getSize(); i++) {
        std::cout << vec2[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}

 

요렇게 하면, 어느 부분이 틀렸는 지 알고 Debugging 하는데 좀 더 편하다.

    SimpleVector<int> vec1 = {1, 2, 3};
    // SimpleVector<int> vec2 = vec1;
    SimpleVector<int> vec2 = {1, 2, 3};

 

이제는 다른 실습을 해 볼 차례

Operator Overloading Exercise

이번 실습의 목표는, Class template을 implement 해보고 Operator Overloading을 진행하는 것이다.

• Learn how to design a generic class that handles multiple data types.
• Implement custom operators such as [], !=, *, and ^.

 

그리고,, 알아두면 좋을 거 같은 lvaue / ravlue의 차이

📌 lvalue와 rvalue 개념 정리 & 예제 설명

✅ 1️⃣ lvalue와 rvalue란?

  1. lvalue (좌측값, Left Value)
    • 메모리에 존재하며 이름이 있는 변수 (즉, 다시 사용할 수 있는 값)
    • 대입 연산자의 왼쪽에 올 수 있음 (p1 = 10; ✅ 가능)
    • 변수처럼 메모리에 저장된 값
  2. rvalue (우측값, Right Value)
    • 임시적으로 생성된 값, 즉 표현식의 결과
    • 대입 연산자의 오른쪽에만 올 수 있음 (10 = a; ❌ 불가능)
    • 임시 객체, 함수 반환값, 상수

✅ 2️⃣ 복사 생성자와 이동 생성자의 차이

📌 Test 1: 복사 생성자

p1은 lvalue
✅ 복사 생성자가 호출됨 (Point(const Point& other))
✅ p2가 p1의 복사본을 받음 (p1은 그대로 유지)

📌 Test 2: 이동 생성자

✅ std::move(p1)은 p1을 rvalue로 변환
✅ 이동 생성자 Point(Point&& other) 호출됨
✅ p3가 p1의 값을 가져오고, p1은 초기화됨 (0,0으로 설정됨)

✅ 3️⃣ Point& other와 Point&& other 차이

📌 Point& other를 사용했을 때

  • std::move(p1)의 반환값(Point&&)을 받을 수 없음 ❌
  • 이동이 아니라 일반 복사처럼 동작함 ❌

이건 솔직히 copy / move 동작구현을 위해 그냥 암기성(?)으로 외워야 되는 거 같다.

특히 copy는 그렇다쳐도 move 동작을 구현시키려면 std::move를 써야 하는데 이것이 rvalue만 받을 수 있어서 이 사단이 나는 거 같다. 완전 copy / move 전용 logic이라 좀 와닿진 않음..;;

 

아래 코드 중 exercise_2.h 에 coding을 진행하며 참고사항을 주석으로 달아 놓았고,

test_case.cpp는 말 그대로 header file 코딩 내용을 test 해보는 코드이다.

 

Empty Version Skeleton 코드와 모범답안은 글 맨 위에 코드가 첨부되어 있다~~!!

13_exercise_2.h

#ifndef EXERCISE_H
#define EXERCISE_H

#include <iostream>
#include <cmath>
#include <initializer_list>
// #include <vector>
using namespace std;

template<typename T>
class Point {
private:
    T x, y;  

public:

    Point(T xVal = 0, T yVal = 0) : x(xVal), y(yVal) {

    }


    T getX() const { return x; }
    T getY() const { return y; }
    /*=============================================== TODO 1 ===============================================
        1. Instruction:
            - Implement a copy constructor for the Point class.
        2. Example:
            - When calling ```Point<int> p1(3, 4); Point<int> p2 = p1;```, p2 should be a copy of p1.
    =======================================================================================================*/
    // Move와 Copy Semantic을 위한 처사임
    Point(const Point& other) : x(other.x),y(other.y){
        std::cout << "Copy called" << std::endl;
    }

    /*=============================================== TODO 2 ===============================================
        1. Instruction:
            - Implement a move constructor for the Point class.
        2. Example:
            - When calling ```Point<int> p1(3, 4); Point<int> p2 = std::move(p1);```, 
              p2 should take the values from p1, and p1 should be reset.
    =======================================================================================================*/
    // Move만을 위한 only only!!! 생성자
    Point(Point&& other) : x(other.x),y(other.y){
        other.x = 0;
        other.y = 0;
        std::cout << "Move called" << std::endl;
    }
    /* 아, TODO 1~2 는 Constructor 단계부터 바로 Copy or Move하는 것
    TODO 3~4는 Point를 만들어 놓고, 그 다음 Copy or Move하는 것 */

    /*=============================================== TODO 3 ===============================================
        1. Instruction:
            - Implement the copy assignment operator for the Point class.
        2. Example:
            - When calling ```Point<int> p1(3, 4); Point<int> p2; p2 = p1;```,
              p2 should have the same values as p1.
    =======================================================================================================*/
    void operator=(const Point& other){
        x = other.x;
        y = other.y;
    }
    // // 모범답안
    // Point& operator=(const Point& other) {  
    // if (this != &other) {  // ✅ 자기 자신과의 대입 방지
    //     x = other.x;
    //     y = other.y;
    // }
    // return *this;
    // }
    /*=============================================== TODO 4 ===============================================
        1. Instruction:
            - Implement the move assignment operator for the Point class.
        2. Example:
            - When calling ```Point<int> p1(3, 4); Point<int> p2; p2 = std::move(p1);```, 
              p2 should take the values from p1, and p1 should be reset.
    =======================================================================================================*/
    // 이건 std::move 의 특징이 rvalue를 반환하기 때문에 Point&&로 표현한 것이다. Point && -> Reference를 2번 하면 rvalue, 1번이면 우리가 알던데로 lvalue를 뜻한다.
    Point& operator=(Point&& other){ 
        if(this != &other){
            x = other.x;
            y = other.y;
            other.x = 0;
            other.y = 0;
        }
        return *this;
    }
    /*=============================================== TODO 5 ===============================================
        1. Instruction:
            - Implement an assignment operator for the Point class using initializer list .
        2. Function Definition:
            - If the initializer list does not have exactly two elements, throw an invalid_argument exception.
              `` You must provide exactly 2 values (for x and y). ``
        3. Example:
            - When calling ```Point<int> p1; p1 = {3, 4};```, 
              p1 should have x = 3 and y = 4.
    =======================================================================================================*/
    Point& operator=(const std::initializer_list<T> lst){ // std::initializer_list<T>& 로 받으면 error catch 못함..
        if(lst.size()!=2){
            throw invalid_argument("You must provide exactly 2 values (for x and y).");
        }
        auto it = lst.begin();
        x = *it;
        it++;
        y = *it;
        return *this;
    }
    // 내 오답.. vector 이런 거 쓰는 게 아니라 initializer_list가 따로 있었다.
    // Point& operator=(const vector<T>& lst){
    //     int n = lst.size();
    //     x = lst[0];
    //     y = lst[1];
    //     return *this;
    // }
    /*=============================================== TODO 6 ===============================================
        1. Instruction:
            - Implement the [] operator to access the x or y coordinates using an index.
        2. Function Definition:
            - If the index is anything other than 0 or 1, throw an out_of_range exception.
              `` Index must be 0 or 1. ``
        3. Example:
            - p[0] should return the x coordinate, and p[1] should return the y coordinate.
    =======================================================================================================*/
//중요한 개념!!!!!!!!!!!!!!!!!!!!
    T& operator[](const int idx){ // Reference를 반환해야 rhs로 data 수정이 가능
        if(idx==0) return x;
        else if(idx==1) return y;
        else throw out_of_range("Index must be 0 or 1.");
    }

    // // 개판이다. operator[] 로 index 접근하는 걸 아예 몰랐음
    // void operator[](const int idx,const T& val){
    //     if(!(idx==0||idx==1)){
    //         throw out_of_range("Index must be 0 or 1.");
    //     }
    //     if(idx==0){
    //         x = val;
    //     }
    //     else y = val;
    // }

    /*=============================================== TODO 7 ===============================================
        1. Instruction:
            - Implement the * operator to calculate the dot product of two Point objects.
        2. Function Definition:
            - Return the result of the dot product calculation.
        3. Example:
            - When calling ```Point<int> p1(1, 2), p2(3, 4); int result = p1 * p2;```, the result should be 11.
    =======================================================================================================*/
    T operator*(const Point& other) const {
        return x * other.x + y * other.y;
    }
    // // 내 답안도 맞긴 했음
    // T operator*(const Point& other) const{
    //     T ans = 0;
    //     ans += (x*other.x);
    //     ans += (y*other.y);
    //     return ans;
    // }

    /*=============================================== TODO 8 ===============================================
        1. Instruction:
            - Implement the ^ operator to calculate the norm (length) of a Point object.
            - Support both the L1 norm and L2 norm.
        2. Function Definition:
            - L1 norm : |x| + |y|.
            - L2 norm : sqrt(x^2 + y^2).
            - If the norm type is not 1 or 2, throw an invalid_argument exception.
              `` Invalid norm type. Use 1 for L1 norm and 2 for L2 norm. ``
        3. Example:
            - When calling ```Point<int> p1(3, 4); double result = p1^2;```, the result should be 5.0.
    =======================================================================================================*/
    double operator^(int idx) const {
        if(idx==1) return abs(x)+abs(y);
        else if(idx==2) return sqrt(pow(x,2)+pow(y,2));
        else throw invalid_argument("Invalid norm type. Use 1 for L1 norm and 2 for L2 norm.");
    }

    // Return type을 T로 하니까 L2에서 걸리네 왜지..? Double로 하면 Pass -> T로하니까 root(5)가 2로 계산되어서 그런듯
    // T operator^(const int& idx) const{
    //     T ans = 0;
    //     if(idx==1){
    //         if(x>=0) ans += x;
    //         else ans -= x;
    //         if(y>=0) ans += y;
    //         else ans -= y;
    //         return ans;
    //     }
    //     else if(idx==2){
    //         return sqrt(x*x + y*y);
    //     }
    //     else throw invalid_argument("Invalid norm type. Use 1 for L1 norm and 2 for L2 norm.");
    // }

    
    /*=============================================== TODO 9 ===============================================
        1. Instruction:
            - Implement the != operator to check if two Point objects are not equal.
        2. Function Definition:
            - The != operator should return true if the x or y coordinates of the two points are different. 
            - Otherwise, it should return false.
        4. Example:
            - When calling ```Point<int> p1(3, 4); Point<int> p2(3, 5); bool result = (p1 != p2);```, 
              the result should be `true` because the `y` coordinates are different.
            - It should return `false` if both `x` and `y` are the same.
    =======================================================================================================*/
    bool operator!=(const Point& other) const {
        return x != other.x || y != other.y;
    }
    // // 나도 정답~
    // bool operator!=(const Point& other) const {
    //     if(x!=other.x || y!=other.y) return true;
    //     return false;
    // }



    /////////////// DO NOT MODIFY THIS ////////////////
    friend ostream& operator<<(ostream& os, const Point<T>& point) {
        os << "(" << point.x << ", " << point.y << ")";
        return os;
    }
    // // 걍 함 만들어 봄
    // friend ostream& operator<<(ostream& out, const Point<T>& point){
    //     out << "(" << point.x << "," << point.y << ")";
    //     return out;
    // }
    /////////////// DO NOT MODIFY THIS ////////////////
};



#endif

 

13_test_cases_2.cpp
#include <iostream>
#include <cassert>
#include "13_exercise_2.h"  


void runTests() {

    std::cout << "Test 1: Copy constructor\n";
    Point<int> p1(3, 4);
    Point<int> p2 = p1;
    assert(p1.getX() == p2.getX() && p1.getY() == p2.getY());  
    std::cout << "Test 1 passed: p1 = " << p1 << ", p2 = " << p2 << "\n";

    std::cout << std::endl;

    std::cout << "Test 2: Move constructor\n";
    Point<int> p3 = std::move(p1);
    assert(p3.getX() == 3 && p3.getY() == 4);  
    assert(p1.getX() == 0 && p1.getY() == 0);  
    std::cout << "Test 2 passed: p3 = " << p3 << ", p1 (moved) = " << p1 << "\n";

    std::cout << std::endl;

    std::cout << "Test 3: Copy assignment operator\n";
    Point<int> p4(10, 20);
    p4 = p3;
    assert(p4.getX() == p3.getX() && p4.getY() == p3.getY());  
    std::cout << "Test 3 Passed: p4 = " << p4 << "\n";

    std::cout << std::endl;

    std::cout << "Test 4: Move assignment operator\n";
    Point<int> p5(15, 25);
    p5 = std::move(p4);
    assert(p5.getX() == 3 && p5.getY() == 4);  
    assert(p4.getX() == 0 && p4.getY() == 0); 
    std::cout << "Test 4 passed: p5 = " << p5 << "\n";

    std::cout << std::endl;

    std::cout << "Test 5: Initializer list assignment\n";
    Point<int> p6;
    p6 = {5, 10};  
    assert(p6.getX() == 5 && p6.getY() == 10);
    std::cout << "Test 5 Passed: p6 = " << p6 << "\n";

    std::cout << std::endl;


    std::cout << "Test 6: [] Operator Access\n";
    assert(p6.getX() == 5 && p6.getY() == 10);
    p6[0] = 7;
    p6[1] = 14;
    assert(p6.getX() == 7 && p6.getY() == 14);
    std::cout << "Test 6 Passed: p6 = " << p6 << "\n";

    std::cout << std::endl;


    std::cout << "Test 7: Dot Product\n";
    Point<int> p7(1, 2);
    Point<int> p8(3, 4);
    int dot_product = p7 * p8;  
    assert(dot_product == 11);  
    std::cout << "Test 7 Passed: Dot product of p7 and p8 = " << dot_product << "\n";

    std::cout << std::endl;


    std::cout << "Test 8: L1 Norm\n";
    double l1_norm = p7 ^ 1; 
    assert(l1_norm == 3); 
    std::cout << "Test 8 Passed: L1 norm of p7 = " << l1_norm << "\n";

    std::cout << std::endl;


    std::cout << "Test 9: L2 Norm\n";
    double l2_norm = p7 ^ 2; 
    assert(abs(l2_norm - 2.236) < 1e-3);  
    std::cout << "Test 9 Passed: L2 norm of p7 = " << l2_norm << "\n";

    std::cout << std::endl;


    std::cout << "Test 10: Inequality Operator\n";
    assert(p7 != p8);  
    std::cout << "Test 10 Passed: p7 != p8\n";

    std::cout << std::endl;


    std::cout << "Test 11: Invalid initializer list assignment (too few elements)\n";
    try {
        Point<int> p9;
        p9 = {3};  
        std::cout << "Test 11 Failed: Exception not thrown for invalid initializer list.\n";
    } catch (const std::invalid_argument& e) {
        std::cout << "Test 11 Passed: Exception caught: " << e.what() << "\n";
    }


    std::cout << std::endl;


    std::cout << "Test 12: Invalid initializer list assignment (too many elements)\n";
    try {
        Point<int> p10;
        p10 = {1, 2, 3};  
        std::cout << "Test 12 Failed: Exception not thrown for invalid initializer list.\n";
    } catch (const std::invalid_argument& e) {
        std::cout << "Test 12 Passed: Exception caught: " << e.what() << "\n";
    }

    std::cout << std::endl;


    std::cout << "Test 13: Invalid [] operator access (out of bounds)\n";
    try {
        Point<int> p11(3, 4);
        int value = p11[2]; 
        std::cout << "Test 13 Failed: Exception not thrown for out-of-bounds access.\n";
    } catch (const std::out_of_range& e) {
        std::cout << "Test 13 Passed: Exception caught: " << e.what() << "\n";
    }

    std::cout << std::endl;


    std::cout << "Test 14: Float dot product\n";
    Point<float> p12(1.5f, 2.5f);
    Point<float> p13(3.0f, 4.0f);
    float dot_product_2 = p12 * p13;  
    assert(dot_product_2 == 1.5f * 3.0f + 2.5f * 4.0f);
    std::cout << "Test 14 Passed: Dot product of p11 and p12 = " << dot_product_2 << "\n";

    std::cout << std::endl;


    std::cout << "Test 15: Floating-point L2 Norm\n";
    double l2_norm_2 = p12^2;  
    assert(abs(l2_norm_2 - sqrt(1.5 * 1.5 + 2.5 * 2.5)) < 1e-6);
    std::cout << "Test 15 Passed: L2 norm of p11 = " << l2_norm_2 << "\n";

    std::cout << std::endl;


    std::cout << "Test 16: Invalid norm type\n";
    try {
        double invalid_norm = p12^3;  
        std::cout << "Test 16 Failed: No exception thrown for invalid norm type.\n";
    } catch (const std::invalid_argument& e) {
        std::cout << "Test 16 Passed: Exception caught: " << e.what() << "\n";
    }
}

int main() {
    try {
        runTests();
        std::cout << "\nAll test cases passed successfully.\n";
    } catch (const std::exception& e) {
        std::cerr << "Test failed: " << e.what() << "\n";
        return 1;
    }

    return 0;
}