반응형

개요

ubuntu 20.04 LTS 에서 OpenCV C++ 기본 빌드 환경에 대해서 샘플로 작성한 포스트. 해당 샘플은 카메라로 받은 이미지를 OpenCV imshow() 를 통해서 보여준다.

기본 틀이기 때문에 처음에는 복붙으로 확인이후 자신이 개발하고자 하는 것에 맞춰서 수정해 나가면 된다. 물론 makefile 기반이다.

굳이 camera 코드를 샘플로 작성한 이유는 임베디드 환경에서 카메라 다루게 되는 경우 성능상 C++을 써야 하기 때문이다. 특별한 이유가 없다면 가능하면 그냥 opencv-python 을 사용하자(하지만 제품을 만들려면 저사양 고성능을 뽑아야 하느라 삽질하겠지).

makefile sample

makefile 에 대해서 배운적이 없다면 복붙전에 읽어보고 사용하는 것이 좋다. 파일명은 반드시 makefile로 지어야 한다.  make clean 을 할 경우 규칙에 의해서 빌드 결과와 실행 파일이 삭제 되도록 되어 있다.

# Makefile
CPPFLAGS = -std=c++14 $(shell pkg-config --cflags opencv4)
LDLIBS = $(shell pkg-config --libs opencv4)
# source file name
OBJS = webcam_sample.o
# result execute file name
TARGET = test

all: $(TARGET)

$(TARGET): $(OBJS)
	g++ $(CPPFLAGS) -o $@ $< $(LDLIBS)

clean:
	rm -f *.o
	rm -f $(TARGET)

camera sample code

널리 알려져 있는 camera sample code에서 실행시 카메라 번호를 수정할 수 있도록 약간만 수정한 소스코드이다. 

// webcam_sample.cc
#include <opencv4/opencv2/videoio.hpp>
#include <opencv4/opencv2/highgui.hpp>
#include <iostream>
#include <string>

bool is_number(const std::string& s)
{
// 
    return !s.empty() && std::find_if(s.begin(), 
        s.end(), [](unsigned char c) { return !std::isdigit(c); }) == s.end();
}

int main(int argc, char** argv)
{
    int cam_num = 0;
    if (2 == argc) {
        auto camNumParam = std::string(argc[1]);
        if (is_number(camNumParam)) {
            cam_num = std::atoi(camNumParam);
        }
    }

    cv::VideoCapture cap(cam_num);
    if(!cap.isOpened()) {
        std::cout << "did not open camera" << std::endl;
        return -1;
    }
    cv::Mat frame;
    while(1)
    {
        cap >> frame;
        cv::imshow("Camera Capture", frame);
        if (cv::waitKey(30) >= 0)
        {
            break;
        }
    }
    return 0;
}

카메라 장치 다룰때 주의사항

우분투는 먼저 인식한 카메라 장치 순으로 번호를 부여한다. 일반적으로 웹에 돌아다니는 예제는 0번으로 되어 있지만, 노트북같은 환경에서 usb 카메라를 붙이면, 노트북 기본 카메라가 0번, usb web cam은 1번으로 잡히게 된다.

또한 다른 프로그램에서 카메라를 점유(사용)하고 있을 경우에는 다른 프로그램에서 접근을 할 수 없다.

반응형

'Computer Vision' 카테고리의 다른 글

Computer Vision 이미지 자료형  (0) 2024.01.18
Ubuntu 20.04 OpenCV 4.x + cuDNN 설치  (0) 2023.04.26
반응형

C++ 컴파일 에러 : cannot bind ~

에러 메시지

cannot bind 'std::basic_ostream<char>' lvalue to 'std::basic_ostream<char>&&'


발생하는 사례

 보통 cout 표준 클래스에서 자주 발견된다. 뿐만 아니라 연산자 오버라이딩이 된 클래스를 사용하다 보면 종종 발생된다.


에시1

// c++11 more than
#include <iostream>

using namespace std;

enum class COLOR
{RED, GREEN, BLUE};

int main(const int argc, const char * argv[])
{
    cout << "Color enum Red : "
        << static_cast<int>(COLOR::RED) << endl;    // No Error
    cout << "Color enum Green : " << COLOR::GREEN << endl;    // Error!
    return 0;
}




예시2

 이 예시는 보기 조금 민망하다..

// c++11 more than
#include <iostream>
#include <vector>

using namespace std;

int main(const int argc, const char * argv[])
{
    cout << "vectors : " << vector<int>{1, 2, 3} << endl;    // Error!
    return 0;
}




원인

 입출력 관련 클래스가 연산자 오버라이딩이 가능해지면서 직관적으로 코드를 작성할 수 있게 되었는데, 그러다 보니 정의 되지 않은 타입을 연산자로 받을 경우 발생되는 간단한 에러이다. 별거 아닌 에러이지만, 사람이 종종 실수를 할 수 있기 때문에 볼 수 있다. 다만, 메시지의 의미를 파악하는 연습을 하도록 하자.

 해결 방법은 연산자가 이해할 수 있는 타입으로 캐스팅을 해주거나 연산자 오버라이드를 추가 해주면 된다. 이 글을 작성하는 시점에서는 이런 실수를 방지 하기 위해 연산자 오버라이딩은 가능하면 자제하는 추세인걸로 알고 있다.


참고자료

스택오버플러워 질문



반응형

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

C++11 : 범위기반 for 문  (0) 2018.03.04
C++11 : STL std::array 정적배열  (0) 2018.03.04
C++ Socket : 고정 구조체  (0) 2017.08.04
C++11 : 연역적 선언 auto (입문)  (0) 2017.07.03
C++ 패턴 : 동적 싱글턴(Dynamic Singleton)  (0) 2017.06.16
반응형

CMake : 공식예제 step1 해보기


MS의 VS 시리즈 통합개발환경을 사용하다가 리눅스 같은 유닉스 계열에서 비통합개발환경에서 여러 소스파일을 컴파일을 할 경우 이를 도와주는 도구가 shell, makefile, CMake 등이 있다.

 소스파일이 많아지고 복잡해질 수록 shell 보다는 makefile, makefile보다는 CMake가 비교적 적합하다. 특히 CMake의 경우 윈도우에서도 빌드할 수 있기 때문에, 소스코드 생성시 의 운영체제의 고유 API를 사용하지 않았다면, 동일한 소스파일을 빌드할 수 있다. 이를 이용해서 OpenCV등 오래전 부터 지속된 오픈소스 프로젝트들이 CMake를 사용해서 사용되어져 왔다.


 문제는 이러한 강력한 기능이 있음에도 불구하고 튜토리얼 문서는 관리가 안되고 있는 듯하다. 이 글을 기록하는 시점에 3.5.1버전이 필자의 리눅스에 설치되어 있지만, 공식 홈페이지의 메인 튜토리얼에는 여전히 2.6 버전으로 되어 있다. 게다가 상당히 난잡하게 기록되어 있다. 아마도 이를 통해서 진입장벽을 만들고 싶었던건지 아니면, 기록이 귀찮았던건지는 물어보지 않아서 알 수 없다.


 일단은 예시 들을 진행을 해보면서, 발생되는 문제점을 해결하면서 기록을 하고자 한다.


CMake 개요

 cmake는 CMakeLists.txt 명으로 된 파일을 스크립트로 인식하여 makefile을 생성하여 최종적으로는 makefile(실행은 make)을 사용하여 실행 및 공유파일을 생성한다.




예시 소스코드


// tutorial.cxx

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"

int main(const int argc, const char * argv[])
{
    double inputValue;
    double outputValue;
    if(argc < 2)
    {
        fprintf(stdout, "usage: %s number\n", argv[0]);
        return 1;
    }
    inputValue = atof(argv[1]);
    outputValue = sqrt(inputValue);
    fprintf(stdout, "The square root of %g is %g\n",
        inputValue, outputValue);

    return 0;
}


 확장자가 cxx이지만 헤더파일을 살펴보면 c인것을 알 수 있다. "TutorialConfig.h"은 CMake에서 설정하는 파일로 빌드 직전에 생성된다.



예시 헤더파일


// TutorialConfig.h.in

// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@


"@"로 감싼 부분은 CMake 파일에서 후에 정의 된 값이 들어가서 작성하게 된다.



예시 CMakeLists.txt


# CMakeLists.txt

cmake_minimum_required(VERSION 2.6)
project(Tutorial)
# The version number
set(Tutorial_VERSION_MAJOR 1)
set(Tutorial_VERSION_MINOR 0)

# configure a header file to pass scma of the CMake settings
# to the source code
configure_file(
    "${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
    "${PROJECT_BINARY_DIR}/Tutorialconfig.h"
)

# add the binary tree to the search path ofr include files
# so that we will find TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")

# add the executedable
add_executable(Tutorial tutorial.cxx)


만약에 여기 까지 진행을 했는데, math.h에 관련된 에러가 발생한다면, 위의 마지막줄 add_executable() 직전에 한줄을 추가 해주자


# add link libraries
LINK_LIBRARIES(m)

주석에 표기 되어 있는대로 라이브러리를 추가 시켜준다. 원래는 cmake가 알아서 추가 시켜줘야 하는데, 간혹 안되는 경우가 있으니 참고 하도록 하자. 위의 한줄은 math.h를 include 했을 때 컴파일 과정중 링크 단계에서 -lm을 하는 것과 같은 효과가 있다.


CMake에서는 콤마(,)가 없다는 것을 주의 해야 한다.

project(Tutorial)은 이름 그대로 프로젝트 이름을 정의해준다.


 set() 키워드는 CMake내에서 변수를 정의 혹은 초기화를 해준다. set(Tutorial_VERSION_MAJOR 1)이라고 하면, Tutorial_VERSION_MAJOR이라는 변수 없다는 생성해서 초기화를 시킨다.


add_executeable(Tutorial tutorial.cxx)는 결과 파일이름이 Tutorial로 되고 여기에 포함되는 소스가 위에 열거가 된다. 예제에서는 하나의 소스파일이지만, 실제로는 여러개의 소스파일이 사용된다.


참조 자료

공식 튜토리얼(영문)

개발자 블로그



반응형

'각종 툴' 카테고리의 다른 글

[PyCharm]기본 키 맵 해제  (0) 2020.12.20
CMake 익히기 : 기초문법 (2)  (0) 2018.02.21
CMake 익히기 : 기초문법 (1)  (0) 2018.02.19
반응형

C에서 간단한 dll을 만들어 C++에서 사용하기


 C에서는 MVC++의 C++와 달리 "#pragma" 매크로를 지원하지 않는다. 하지만, C에서는 전통적으로 다른 파일에서 사용할 수 있도록 하는 "extern"을 키워드가 있다.


 다음은 MSDN의 간단한 예시를 조금 수정했다. 


내보내기 예시)

 MSDN의 예제의 경우 __declspec( dllexport )로 작성된 부분이 __declspec( dllimport )로 작성이 되어 있는데, MSDN측 오타로 생각된다.

// MyCFuncs.h
#ifdef __cplusplus
extern "C" {  // only need to export C interface if
                  // used by C++ source code
#endif

__declspec( dllexport ) void Hello(void);

#ifdef __cplusplus
}
#endif


// MyCFuncs.c
#include "MyCFuncs.h"
#include <stdio.h>

void Hello(void)
{
    puts("Hello world");
}


dll 들여오기(Import) 공통 작업

 C++의 dll 파일을 링크하는 것과 비슷한 준비 작업이 필요하다. 실행 프로그램의 프로젝트를 생성한다. 여기서는 "Windows 데스크탑"의 빈프로젝트로 생성한다.

해당 프로젝트 폴더로 내보내는 예시에서 만든 lib파일과 헤더파일을 복사한다.

"솔루션 탐색기"에서 "헤더 파일"항목에 마우스 우클릭을 한뒤 "추가 -> 기존항목"에서 복사한 헤더파일을 추가한다.



들여오기 예시1)

 "솔루션 탐색기"에서 해당 프로젝트 명을 우클릭하여 "프로젝트 속성" 창을 연다.

프로젝트 속성의 "링크 -> 일반"의 "추가 라이브러리 디렉터리" 항목에 lib 파일이 있는 폴더 경로를 추가 입력해준다.

프로젝트 속성의 "링크 -> 입력"의 "추가 종속성"항목에 lib파일 이름과 확장자 명을 추가 입력을 한다.

아래의 소스코드를 작성한다.

// MyCFuncsRef.cpp
#include "MyCFuncs.h"

int main(const int & argc, const char * argv[])
{
    Hello();
    return 0;
}


컴파일 후 프로그램을 실행할때에는 실행하는 폴더안에 해당 dll 파일이 반드시 있어야한다.



들여오기 예시2)

아래와 같이 매크로 "#pragma comment(lib, " ./MyCfuncs.lib")를 선언부 입력을 해준다.


// MyCFuncsRef.cpp
#pragma comment(lib, "./MyCFuncs.lib")

#include "MyCFuncs.h"

int main(const int & argc, const char * argv[])
{
    Hello();
    return 0;
}


컴파일 후 실행파일이 있는 폴더 안에 해당 dll 파일이 있어야 정상적으로 실행된다.



정리

 C의 경우 클래스와 같은 자료형을 지원하지 않기 때문에 단지 헤더파일 부분만 주의 깊게 작성하면 된다. 다만, C++에서는 해당 헤더파일이 C++에서 사용된다는 것을 알리기 위하여 매크로 변수 __cplusplus를 이용 하여 extern "c" { 항목을 활성 혹은 비활성 한다는 점을 기억하자.


 dll참조 함수의 성능을 높이기 위해 포인터로 함수 주소를 가져와서 호출하는 방법도 있는데, 이부분은 추가로 더 조사를 해봐야 될것 같다.



참고자료

MSDN C 함수를 ~ 내보내기




반응형
반응형

C++에서 간단한 dll를 만들어 C++에서 사용하기

 MS사에서 제공하는 MVC++ 컴파일러는 모던 C 표준 지원을 제대로 하지 않는다. 이러한 이유 때문인지 프로그래밍 입문 과목중에 C/C++이라는 과목이 존재하는 듯 하다.


 MVC++에서는 함수를 다른 프로그램에 사용할 수 있도록 내보내거나 들여오는 매크로를 지원한다. MSDN에서는 간단한 예시를 제공한다.


내보내기 예시)

우선 MathFuncsDll 이름의 프로젝트를 생성한다. 생성할때 dll프로젝트로 생성해야 하는데, VS 2017부터는 기존 마법사로 생설할 경우 "Visual C++" 하위의 "Windows 데스크톱" 항목의 Windows 데스크톱 마법사에서 생성하면된다. 마법사가 귀찮다면, DLL로 생성하면 된다. 

// MathFuncsDll.h

#ifdef MATHFUNCSDLL_EXPORTS
#define MATHFUNCSDLL_API __declspec(dllexport)
#else
#define MATHFUNCSDLL_API __declspec(dllimport)
#endif

namespace MathFuncs
{
    // This class is exported from the MathFuncsDll.dll
    class MyMathFuncs
    {
    public:
        // Return a + b
        static MATHFUNCSDLL_API double Add(double a, double b);

        // Return a - b
        static MATHFUNCSDLL_API double Subtract(double a, double b);

        // Return a * b
        static MATHFUNCSDLL_API double Multiply(double a, double b);

        // Return a / b
        // Throws const std::invalid-argument & if b is 0
        static MATHFUNCSDLL_API double Divide(double a, double b);
    };
}


위의 매크로 함수를 정의 하는데, dll프로젝트일 경우 컴파일러의 전처리 옵션에 [대문자 프로젝트 명] + "_EXPORTS"(여기서는 "MATHFUNCSDLL_EXPORTS")의 형태의 매크로가 자동으로 추가 되어 있다. 이러한 특징을 이용해서 헤더 파일에 DLL 프로젝트의 경우 내보내는 매크로 함수"__declspec(dllexport)"가 되고 그외 프로젝트에서는 가져오는 매크로 함수 "__declspec(dllimport)"로 치환이 된다.

 이렇게 만들어진 헤더 파일은 나중에 참조하는 실행 프로그램에서도 include 해야 한다.


// MathFuncsDll.cpp

#include <iostream>
#include "mathfuncsdll.h"

using namespace std;

namespace MathFuncs
{
    double MyMathFuncs::Add(double a, double b)
    { return a + b; }

    double MyMathFuncs::Subtract(double a, double b) 
    { return a - b; }

    double MyMathFuncs::Multiply(double a, double b)
    { return a * b; }

    double MyMathFuncs::Divide(double a, double b)
    {
        if(b == 0)
        {
            throw invallid_argument("b cannot be zero");
        }
        return a / b;
    }
}

 cpp 파일까지 작성하였다면 빌드를 하면 된다. 이때 "win32"로 했는지 "x64"으로 했는지 구분하는 것이 좋다. 에러가 없다면, 가능하면 Release로 빌드하여 최적화를 하자.



들여오기 사용 예시1)

 실행하는 프로젝트의 경우 선행 작업이 조금 필요하다. 우선 새로운 프로젝트를 만들자 이름은 대략 "MyExceRefDll"로 빈프로젝트로 만들어도 무난하다. 앞의 프로젝트에서 빌드한 파일중에 .lib와 dll 파일이 생성된 것을 알 수 있다. 빌드와 링크과정에는 헤더파일과 lib 파일이 필요하다. 헤더파일은 솔루션 탐색기에 추가를 시켜야 한다. "솔루션 탐색기 -> 헤더파일 -> 우클릭 -> 추가 -> 기존항목 -> 헤더파일"


// MyExecRefsDll.cpp

#include <iostream>
#include "MathFuncsDll.h"

using namespace std;

int main(void)
{
    double a = 7.4;
    int b = 99;

    cout << "a + b = " << MathFuncs::MyMathfuncs::Add(a, b) << endl;
    cout << "a - b = " << MathFuncs::MyMathfuncs::Subtract(a, b) << endl;
    cout << "a * b = " << MathFuncs::MyMathfuncs::Multiply(a, b) << endl;
    cout << "a / b = " << MathFuncs::MyMathfuncs::Divide(a, b) << endl;

    try
    {
        cout << "a / 0 = " << MathFuncs::MyMathFuncs::Divide(a, 0) << endl;
    }
    catch(const invalid_argument & e)
    {
        cout << "Caught exception: " << e.what() << endl;
    }

    return 0;
}

 lib파일은 "솔루션 탐색기 -> 프로젝트 -> 우클릭 -> 속성" 에서 "링커 -> 일반" 항목중 "추가 라이브러리 디렉토리"에 lib파일이 있는 경로를 추가 시킨다. 이후 "링커 -> 입력" 항목중 "추가 종속성"에 추가할 lib 파일명을 적어주면 된다. 이 예시에서는 "MathFuncsDll.lib"를 적어주면 된다.


 링크 옵션을 마치면, 컴파일을 한다. 이때 공부할때 습관적으로 F5키로 실행까지 하는데, 실행파일이 있는 폴더에 dll 파일(여기서는 "MathFuncsDll.dll")을 넣고 실행해야 한다. 별다른 오류 메시지 창이 안보이면, 잘 된거다.



들여오기 사용 예시2)

 앞의 예시는 MVC++ 컴파일러의 옵션 값을 넣어서 dll을 참조했다면, 이번에는 옵션 값을 수정하지 않고 참조 해보자. 앞의 예시와 동일하게 헤더파일을 솔루션 탐색기에서 인식할 수 있도록 추가 해야 한다.

 실행 프로그램의 소스 코드중에 MVC++에서 사용하느 매크로 함수 "#pragma comment()"를 사용하여 참조할 lib 파일의 경로와 파일명을 알려주면 된다. 그럼, 앞의 "예시1"의 컴파일 옵션을 컴파일러가 해당 소스를 읽을때 컴파일 옵션을 설정하기 때문에 따로 속성창을 열어서 수정하지 않아도 된다.


// MyExecrefsDll.cpp
// ...
#pragma comment(lib, "../../MathFuncsDll/Debug/MathFuncsDll.lib");
// ...



정리

 원리상 컴파일 옵션으로 작성이 되는 것이고 그 역할을 하는 매크로 함수를 컴파일러 수준에서 제공을 해주는 것을 이용하는 것이다. 이는 헤더파일이 없어도 참조함수를 선언해서 사용할 수 있지만, 복잡하고 많은 수의 함수를 일일히 선언해주는 것은 비 생산적인 일이 때문에 헤더파일은 그대로 사용하는데, 이 때문에 타사의 상용 라이브러리(흔히 dll)를 사용할 때 헤더파일은 대부분 같이 제공해준다.


 여기서는 간단하게 작성했지만, 가능하면 정신건강을 위해서 정리를 하는게 좋다. 들여오기 예시1, 2는 각각 장단점이 있으니 자신이 우선시 하거나 취향대로 사용하면 될 것 같다.


참조자료

MSDN VS2015(한글)

MSDN VS2015(영문)

DLL 작성 및 사용하기(한글 블로그)




반응형
반응형

C++ Socket : 고정 구조체

알게된 배경

 기존 소켓 교재들 대부분이 C로 되어 있는것을 접하다가 C++로 옮기고 나서 소켓 프로그램을 작성할때에 구조체의 크기가 C와 같이 연속적인 형태로 메모리에 저장되지 않는다는 것을 알게 되었기에 기록으로 남긴다.


C Style Struct?

 이제는 고유 키워드가 되어 버렸다. C만 기본적으로 메모리에 직접적으로 올라가는 듯 하다. 혹은 라이브러리로 혹은 이후 표준으로 확장이 되는지 앞으로 확인이 필요할 듯하다.

 이유는 메모리에 사용자가 로드하는 대로 하지 않는 이유는 성능을 향상시키기 위해서 하는 작업이기 때문이다. 성능을 중요시한다면, 후에라도 지원이 될 듯 하지만.. C의 언어 철학은 프로그래머를 믿으라는 것이 모토이기 때문에 안될 가능성도 있다.


키워드

각 컴파일별로 선언하는 방식이 다르다. GCC 컴파일러 기준으로는 언더바로 확장된 키워드를 사용한다. __attribute__((__packed__))를  struct 와 이름 사이에 넣으면 된다.

GCC 예시)

// Header.hpp
struct __attribute__((__packed__)) SHeader
{
    char name[4];
    int32_t number;
    int32_t body_sz;
};



메모리 로드

 만약 키워드가 없을 경우에는 문자열과 숫자형을 구분하여 8바이트 단위로 메모리에 로드하게 된다. 


 예제 예시로 설명하자면, 편의상 8바이트 단위를 row라고 하면, row1에 name[4]가 로드되고 row2에 number, body_sz가 로드되어서 row가 2개이므로 총 16바이트를 차지하게 된다.

 따라서 __attribute__ 를 사용해서 실제 내부 총량이 메모리에 로드되는 것과 동일하게 하면, 12바이트가 된다(소켓 프로그램에서는 이렇게 되어야 한다).


이러한 이유로 일반적인 PC간의 소켓 통신을 할경우 헤더같이 struct로 자료형을 만들 경우 가능하면 8바이트로 딱떨어지는 형태가 되는 것이 성능저하가 적을 것이라 예측이 되는 요소이다(대부분 운영체제는 현시점에서 8바이트 형태로 최적화 되어 있는 듯 하다. 물론 이는 절대적이지 않다).


(MVC++(Microsoft Visual C++)컴파일러의 경우 다른 방법이 있는 것으로 알고 있는데, 정리할 때 메모부분을 분실해서 추후에 추가하기로 기약한다.)


여담

 C++98 이후 부터 성능을 올리기 위해서 도입이 된 것같다.

 이런식으로 언더바(_) 두개를 이용한 새로운 키워드로 기능을 확장을 하고 있는 추세이기 때문에 변수 선언시 언더바 사용을 자제할 것을 권장하고 있다. C#에서는 클래스 멤버변수를 선언시 언더바를 아직 사용하는 경향이 있지만, C++에서는 언더바 사용을 안하고 반만 헝가리식으로 소문자 m을 붙이는 경향도 있다. 어찌 되었든 권장이니 꼭 따를 필요는 없고 프로젝트 규약에 주의하자.


참고자료

스택오버 플로워


반응형
반응형

C++11 : 연역적 선언 auto (입문)

알게된 배경

 혼자 프로젝트를 진행을 하다 보니 가능하면 최신(2017년에 2011년에 재정된 표준을 사용하는 것이 과연 최신 일까?) 표준을 사용하기 위해서 공부하면서 알게 된 것들이다. 최신 기술이 항상 좋은 건 아니지만, 새로운 기술은 항상 이전 기술을 사용하면서 축적된 경험으로 부터 개선점이 나오기 때문에 반대로 나쁘다고만 할 수 도 없고 생각든다.

 해당 내용은 effective modern c++를 많이 참조했다.


auto 키워드

 C++98 에서도 존재하던 키워드였지만, C++11에서 급진적으로 바뀐 키워드중 하나다. 즉, C++98에도 역할이 다른 키워드이니 C++11을 경계로 이전 버전에서는 호환이 안된다. 다만, 다행스러운건 이전 버전에서는 거의 사용이 안되던 키워드였기 때문에 웬만하면 큰 문제가 발생되지 않는다.


 적절한 한글 명칭으로는 '연역적 형식선언' 정도 붙일 수 있다. auto 키워드는 JS와 C#에 존재하는 var와 유사한 역할을 한다. 즉, 객체 혹은 변수를 선언할때 초기화 하는 값에 의해서 형이 정해지는 것이다.

예시)

int a1 = 10;
auto a2 = 10;    // a1 == a2

int b1;
b1 = 10;
auto b2;    // error
b2 = 10;




 초기화 하는 형이 있어야 형을 선언할 수 있기 때문에 반드시 초기화를 해야 하며, auto 키워드는 변수형 뿐만 아니라 객체로도 사용되기 때문에 STL의 iterator를 선언할 경우 상당히 많은 코드를 줄일 수 있다.

예시)

std::vector<int> arrVec = {1, 2, 3, 4, 5};
std::vector<int>::iterator it1 = arrVec.begin();
auto it2 = arrVec.begin();
// it1[1] == it2[1]




장점

 auto 를 사용함으로써 얻는 장점은 3가지로 자주 언급이 된다.

1. 코드의 길이가 줄어듬

2. 초기화 강제화로 실수가 줄어듬

3. 코드의 유지보수(리펙토링)가 쉬워짐

4. 형 선언 실수를 방지할 수 있음


사실 STL의 자료형을 많이 사용해본 입장에서는 1, 2번의 장점이 매우 크게 느껴진다.


단점

1. 대리자(proxy) 형식 때문에 형식을 잘못 연역하는 경우가 있음

2. 코드만 봤을때 직관적이지 않음(이는 IDE의 부가 기능으로 어느정도 해결 할 수 있음)


 1번의 경우 대표적인 예로 vector<bool> 형이 많이 거론된다. bool 값은 추상적으로 1비트로 표시할 수 있기 때문에 vector 템플릿은 bool 형을 받으면 비트형으로 저장을 하게 된다. 즉, vector<bool> bVec(5)를 선언하면, bVec는 1바이트의 5비트를 갖고 있는 것이다. 문제는 C++은 바이트 단위로 처리를 하기 때문에 bool형은 실제로 바이트로 되어 있는데, 이러한 자료형의 이질성을 해소하기 위해 중간에 대리자라는 형식이 존재한다.


 문제는 이렇게 중간에 껴있는 대리자를 통하여 포인터를 넘겨 받을 경우 대리자가 어떻게 구현되었는지에 따라 영향을 받게 된다. 이러한 경우 경우에 따라 연역적 형 선언에 실패하게 된다. 따라서 이러한 경우 명시적으로 선언을 해줘야 한다.


결론

 이러한 auto 키워드 사용은 권장이지 의무는 아니다. 하지만, 장점과 단점을 생각해서 사용하는 것이 더욱 즐겁고 효율적인 코딩이 될 것이다.


참조자료

Effective Modern C++

반응형
반응형

C++ 패턴 : 동적 싱글턴(Dynamic Singleton)

정리하게 된 배경

 은근히 자주 사용되지만, 가끔 기억이 안나서 잘 못 작성한 코드의 오류를 한참 고민을 했기 때문에 후에 있을 시간을 절약하기 위해 기록을 한다.


싱글턴 패턴

 영문으로는 Singleton이라 하기 때문에 싱글톤이라 표기 한곳도 의외로 많지만, 위키백과에서는 싱글턴으로 표기를 하기 때문에 여기서는 싱글턴으로 표기를 하였다. 싱글턴 패턴도 여러 종류가 있지만, 실용적으로 자주 사용되는 동적 싱글턴 패턴에 대해서 소스코드를 적는다.


 싱글턴 패턴은 이름에서도 알 수 있 듯이 하나만 있다는 의미를 내포하고 있다. 즉, 실질적인 객체는 오직 하나이다. 싱글턴 객체를 사용하기 위해서는 싱글턴 내부에 선언되어 있는 인스턴스객체에 접근해서 사용이 가능하다. C/C++에서는 포인터의 개념을 들어 설명을 하면 이해가 쉽지만(물론 포인터를 이해했다는 전제하에) 포인터가 없는 다른 언어에서는 종종 인스턴스 참조와 인스턴스 생성간 혼란이 있는 듯하다.

 

 논리(이론)적 구성은 단하나의 객체 혹은 인스턴스가 존재해야 하며, 추가로 선언을 못하게 해야 한다. 여기서 동적 싱글톤의 경우 프로그램 시작과 관계없이 싱글턴의 인스턴스를 받아오려는 순간 생성이 되어 생성시점이 동적인 특징이 있다.


소스코드


// Singleton.hpp
#include <iostream>

class Singleton
{
private:
    Singleton(void);
    ~Singleton(void);
    static Singleton* _instance;
public:
    static Singleton* getInstance(void);
    void print(void);
};


// Singleton.cpp
#include "Singletion.hpp"

using namespace std;

Singleton* Singleton::_instance = nullptr;

Singleton* Singleton::getInstance(void)
{
    if(nullptr == _instance)
        _instance = new Singleton;
    return _instance;
}

void Singleton::print(void)
{
    cout << "singletion!" << endl;
}


// main.cpp
#include "Singleton.hpp"

Singleton* ins = Singleton::getInstance();
ins->print();
// or
Singleton::getInstance()->print();


주의할 점

 파일을 hpp에서 getInstance()를 정의할 경우에는 상관이 없지만, cpp에서 정의를 할 경우 static 키워드가 앞에 있어서는 안된다. 만약 사용한다면 문법적인 오류로 에러 메시지를 확인하게 된다.


참고자료

경험




반응형
반응형

gcc C++ : 유닉스 라이브러리 만들기 입문

알게된 배경

 리눅스 환경에서 처음에는 간단한 프로그램을 제작을 했지만, 점차 커기게 되자 상당히 많은 양의 파일들이 한폴더에 어지럽게 있자 더 이상 모듈화를 하지 않으면 코드관리가 안될만한 상황이 되어 라이브러리 작성법에 대해서 공부하게 되었다.


염두해야 될 것

 윈도우 개발환경과 달리 리눅스환경의 경우 gcc의 옵션을 활용하는 방법을 알아야 했다. 그리고 편하게 하려면, makefile을 활용해야 하고, 더 편하게 makefile을 만들기 위해서는 makefile을 어느정도 알고 있는 상태에서 cmake를 사용할 줄 알아야 한다.


테스트 샘플 코드

// ~/lib_test/include/sample.h
#pragma once
#inlcude <iostream>

class Sample
{
public:
    Sample(void);
    ~Sample(void);

    void print(void);
};
// ~/lib_test/src/sample.cpp
#include "../include/sample.h"

using namespace std;

Sample::Sample(void)
{}
Sample::~Sample(void)
{}
void Sample::print(void)
{
    cout << "hello snupy?!" << endl;
}
// ~/lib_test/main.cpp
#include "include/sample.h"

int main(int argc, char* argv[])
{
    Sample sam;
    sam.print();
    return 0;
}



정적 라이브러리

 파일 확장자는 a(Archives의 앞글자)이며, object(.o) 파일과 큰 차이는 없다. 정적라이브러리를 통해서 빌드된 프로그램에 정적라이브러리가 포함되기 때문에 프로그램의 용량이 커지는 특징이 있다. 라이브러리에 수정할 내용이 있어서 수정하게 되면, 프로그램을 다시 빌드해야 하는 단점이 있다.


만드는 방법

# ~/lib_test/ 에서 실행할 경우
g++ -c ./src/sample.cpp -o ./obj/sample.o
ar rc ./lib/libsample.a ./obj/sample.o

 첫 줄은 sample.cpp를 sample.o 로 빌드가 되며, 두번째 줄은 libsample.a로 정적 라이브러리로 만들어준다. ar의 명령어는 아카이브(Archives)의 앞의 두글자를 딴 것이다. 또한 리눅스에서는 파일이름앞에 lib를 붙여야 나중에 실행 파일을 빌드할 때 lib로 인식을 하니 붙여줘야 한다.

 만약 해당 코드에 C++11이상 같은 표준라이브러리을 사용한다면, 오브젝트(.o)파일로 빌드할 때 -std=c++11 같은 옵션을 추가해주어야 한다.


링크된 실행파일 만들기

# ~/lib_test/ 에서 실행할 경우
g++ -o test main.cpp -Llib -lsample

 위의 명령어는 main.cpp라는 소스코드를 test라는 이름으로 빌드를 하게 된다. 이때 lib라는 폴더에 있는 sample이라는 이름의 라이브러리를 참조하라는 명령이 된다. 앞에서 작성한 libsample.a 파일을 sample이라는 라이브러리로 인식을 하게 된다.

 정적 라이브러리는 빌드 이후에는 실행파일에 포함이 되기 때문에 빌드이후 sample.a가 없어도 실행이 된다.


공유 라이브러리

 윈도우의 dll(동적 라이브러리)와 비슷한 개념이다. 때문에 공유 라이브러리를 동적 라이브러리라 부르는 곳도 있다. 확장자는 .so(Share Object의 각 앞글자 머리) 공유 라이브러리의 가장 큰 특징은 정적 라이브러리와 달리 특정 공유 라이브러리 파일만 교체를 해도 수정된 내용이 적용이 된다. 즉, 실행파일을 재 빌드할 필요가 없다(물론 파일이 삭제되거나 추가되면 어쩔 수 없이 재컴파일해야 한다).

 이러한 특징으로 나온 개념이 플러그인 이다(라고 많은 책이나 참고자료가 언급한다).


만드는 방법

# ~/lib_test/ 에서 실행할 경우
g++ -fPIC -c ./src/sample.cpp -o ./obj/sample.o
g++ -shared -o ./lib/libsample.so ./obj/sample.o

 첫줄은 고정된 파일로 컴파일을 하여 오브젝트 파일로 빌드를 한다. 이후 두번째 명령에서는 공유 오브젝트 파일을 생성한다. 여기서는 간단히 확장자를 so로 작성했지만, 보통 프로그램 버전을 관리 할 경우 so.1.0.0 이런식으로 숫자를 뒤에 붙여서 관리를 한다. 또한 -Wl 옵션을 사용하여 추가 옵션을 붙이는 경우가 많다(입문자에게는 이러한 부분이 오히려 진입장벽을 높이는 결과가 되는 것 같다)


링크된 실행 파일 만들기

# ~/lib_test/ 에서 실행할 경우
g++ -o test main.cpp -Llib -lsample

 앞의 링크된 실행파일을 만드는 것과 큰 차이가 없다. 하지만, 이렇게 만들어진 실행파일은 바로 실행할 수 없는데, 이유는 정적 라이브러리와 달리 공유 라이브러리는 실행파일에 포함이 안되어 있기 때문에 실행에 필요한 파일을 알려줘야 한다. 여기서 -L옵션은 빌드할때만 사용되는 라이브러리 경로이다.


 다만, 윈도우즈의 동적 라이브러리인 dll의 경우 보통 실행파일이 있는 폴더와 하위 폴더, 그리고 환경변수에 등록된 폴더에서 실행에 필요한 dll파일이 있는지 찾아서 실행한다.


 반면 리눅스 환경에서는 그냥 환경변수만 찾아 본다(어떤이는 이를 버그라 생각을 한다). 따라서 환경변수에 있는 폴더 경로에 공유 라이브러리를 복사하는 방법과 환경변수를 만들어서 등록해줘야 된다.

 때문에 실행하기 전에 ldd 명령어를 통해 의존성 검사로 파일을 실행하는데 필요한 공유 라이브러리가 연결되어 있는지 확인 할 수 있다.

# ~/lib_test/ 에서 실행할 경우
ldd ./test

 이렇게 확인한 의존성 검사에서 "not found"가 없어지도록 해야 실행이 가능해진다.

 공유 라이브러리를 이용해 배포했을 경우 해당 프로그램을 유지 및 삭제 관리를 할 때 어떻게 할 것인지도 고민을 해야 한다.


환경변수 추가 등록

export LD_LIBRARY_PATH = [라이브러리절대경로]


 환경변수를 통해서 라이브러리 위치를 찾을 수 있다면, ldd로 검사했을 때 not found가 더 이상 보이지 않을 것이다. 실행했을 프로그램의 결과가 보이면 성공적으로 라이브러리를 생성하고 실행을 해본 것이다.


 하지만, LD_LIBRARY_PATH라는 환경변수는 디버그용(배포 전단계의 실행 테스트)에서 사용할 것을 권하지 배포하는 방법으로는 적합하지 않다.


보충해야 될 내용

 여기까지 혼자서 테스트 해보는데 성공했다면, gcc에 대한 옵션에 대해서 추가로 살펴봐야 한다. 당연한 소리지만, 테스트 결과 -l 옵션을 여러번 사용해서 여러개의 라이브러리를 참조하는 것이 가능하다. 또한 옵션을 사용하는 경우가 많으니 gcc 옵션에 대해서도 어느 정도 숙지를 하고 있어야 한다.


참고자료

우분투 환경에서 C언어로 배우는 리눅스 프로그래밍(서적)

옵션에 대한 정리(영문)

옵션에 대해 한글로 자주 사용하는 것만 정리(간혹 오역도 보인다)



반응형
반응형

STL C++ : vector 중복원소 제거

알게된 배경

 한대의 관리 PC에서 여러대의 디바이스 리눅스 계열 운영체제에 접속하는 디바이스 IP 리스트를 관리하기 위해서는 중복으로 접속을 하면 안되기 때문에 중복 검사하는 알고리즘이 필요 하게 되었었다. 일단 이 디바이스 정보들은 vector에 읽어놓은 상태이다(list를 사용하지 않은 이유는 일반적으로 디바이스 정보는 초기에만 설정을 해주고 나머지는 주로 읽기 때문에 이러한 구성을 선택하였다).

 기존의 수많은 STL 관련 서적들에서는 주로 숫자로 된 원소만 정렬하기 때문에 과연 실무에서 응용이 될지 의문이 들었었는데, 프로젝트를 하다 보니 응용을 하게 되어 다시 정리하게 되었다.


디바이스 리스트의 중복 검사

 일반적인 방법은 디바이스 정보중에 구분이 가능한 유니크(unique)한 요소로 비교하여 중복이 될 경우에는 바로 삭제하면 좋겠지만, vector에서 비교 도중에 삭제할 경우 index가 변경되는등 예상지 못하는 상황이 발생된다(그리고 코드도 복잡해진다).


 때문에 안전한 방법 생각한것은 다음과 같다.


1. 중복 검사중에는 중복으로 판된된 vector의 index만 따로 뽑아서 다른 vector에 저장을 해놓는다.


 이부분은 상황에 따라서 로직을 만들면 된다. 여기서는 잘 알려진 방법인 버블 정렬 하는 방식으로 비교하되 같은지 여부를 체크한다. 이 경우 2개의 중복이 있을 경우 문제가 안되지만, 3개 이상의 중복 원소가 있을 경우에는 문제가 생긴다. 때문에 이를 보완하기 위해서 2, 3단계들을 추가로 진행하는 것이다.


2. 중복 index가 있는 vector는 int같은 숫자 원소를 갖고 있기 때문에 (대부분 STL 교재의 예제에 있는)일반적으로 중복 검사 알고리즘으로 제거를 한다.


3. 중복 검사가 끝난 index들을 재 정렬을 한다.


4. 3에서 정렬한 vector의 index를 읽어서 제거를 한다. 이때 index가 큰 순서대로(배열의 뒤에서 부터) 제거를 한다.


 vector가 뒤에서 줄어들기 때문에 index가 바뀌는 일도 없고, 데이터 삭제시 이동되는 위치가 다르기 때문에 연산량도 줄어들게 된다.


STL vector 중복 제거

#include <vector>
#include <algorithm>
#include <string>

using namespace std;

struct deviceinfo
{
    string hostname;
    // 기타 요소
};

vector<int> checkOverlap(const vector<deviceinfo> &devVec)
{
    // 중복되는 원소가 보이면, 해당 index를 indexVec에 넣어주는 알고리즘을 구현한다.
    // 여기서는 devVec의 구조체 안에 hostname이 string을 비교한다고 가정한다.
    vector<int> indexVec;
    auto it = devVec.begin();    // auto는 C++11이상에서 사용
    for(int i=0; i<devVec.size(); ++i)
    {
        for(int j=i+1; j<devVec.size(); ++j)
        {
            if(0 == it[i].hostname.compare(it[j].hostname))
                indexVec.push_back(j);
        }
    }
    return indexVec;
}

void rmOverlap(vector<deviceinfo> &devVec)
{
    vector<int> indexVec = checkOverlap(devVec);

    // index중에서 중복되는 부분을 제거
    sort(indexVec.begin(), indexVec.end(), less<int>() );
    vector<int>::iterator pos;
    pos = unique(indexVec.begin(), indexVec.end() );
    indexVec.erase(pos, indexVec.end() );

    // 중복이 제거된 index를 내림차순으로 정렬
    sort(indexVec.begin(), indexVec.end(), less<int>() );
    // 디바이스 vector에서 중복 index부분을 제거
    pos = indexVec.begin();
    for(; pos != indexVec.end(); ++pos)
    {
        devVec.erase(devVec.begin() + (*pos));
    }
}

일반적으로 알려있는 부분은 이미 다른 사람(과 교재)에서 많이 검중이 되었기 때문에 이를 응용할 경우 비교적 안전하다. 중간에 삭제 삽입이 쉬운 리스트로 중복 검사를 한뒤에 vector로 옮겨서 사용하는 방법도 방법중 하나가 된다.


참고자료

블로그: 중복제거에 대해 간략히 나옴(설명은 없음)


추가수정(2017.6.7)

반응형

+ Recent posts