들어가며
앞선 글에서는 C++이 어떤 배경에서 등장했고, 객체지향 프로그래밍이 왜 필요해졌는지, 그리고 어떤 특성을 가지는지 살펴보았다.그렇다면 C++은 처음부터 지금과 같은 모습으로 사용되었을까?당연히 그렇지 않다.초기의 C++은 여러 환경에서 다양한 구현과 버전으로 사용되었으며, 모든 개발자가 따라야 할 하나의 공식적인 기준도 존재하지 않았다.그렇다면 C++이 어떻게 하나의 표준으로 정리되어 현재까지 발전할 수 있었을까?이번 글에서는 C++이 표준화되는 과정과 그 시작을 살펴보고자 한다.
먼저 생각해볼 질문
본문을 읽기 전에 다음 질문에 대해 먼저 스스로 고민해보자.
- C++이 널리 사용되기 위해서는 어떤 기준이 필요했을까?
- 하나의 언어가 여러 환경에서 동일하게 동작하려면 무엇이 필요할까?
- 같은 코드를 여러 자료형에 적용하려면 어떤 방법이 필요할까?
C++ 표준화의 필요성
C++이 여러 개발 환경에서 사용되기 시작하면서 새로운 질문이 생겨났다.
같은 C++ 코드를 작성했는데도 사용하는 컴파일러에 따라 결과가 달라진다면, 그 코드는 정말 같은 언어로 작성되었다고 할 수 있을까?
표준이 없던 시기의 C++
초기의 C++은 하나의 공식 표준을 기준으로 발전한 언어가 아니었다. C with Classes에서 시작한 이후 여러 기능이 추가되었고, 이를 지원하는 다양한 컴파일러와 구현이 등장했다. 하지만 모든 구현이 따라야 할 하나의 규칙이 정해져 있지는 않았다.
각 컴파일러는 공통적으로 지원하는 기능도 있었지만, 특정 컴파일러에서만 제공하는 확장 기능이나 서로 다른 해석을 포함하기도 했다. 따라서 개발자가 작성한 코드는 C++이라는 같은 이름을 사용하더라도, 어떤 컴파일러와 환경을 기준으로 작성되었는지에 따라 다르게 동작할 수 있었다.
한 컴파일러에서 정상적으로 동작하던 코드가 다른 컴파일러에서는 컴파일되지 않을 수 있고, 특정 컴파일러의 확장 기능에 의존한 코드는 다른 환경에서 다시 수정해야 할 수도 있다. 이러한 차이는 개인이 작성하는 작은 프로그램보다 여러 사람이 함께 개발하는 프로젝트나 다른 사용자에게 배포되는 라이브러리에서 더 큰 문제가 된다.
- 같은 코드가 다른 컴파일러에서 컴파일되지 않을 수 있다.
- 컴파일러에 따라 프로그램의 동작이 달라질 수 있다.
- 특정 구현에 의존하는 코드가 늘어날 수 있다.
- 라이브러리와 프로그램 사이의 호환성을 보장하기 어렵다.
- 개발자가 환경마다 다른 규칙을 따로 익혀야 한다.
C++이 널리 사용되기 위해서는 새로운 기능을 추가하는 것만큼이나, 여러 구현이 공통으로 따라야 할 기준을 정하는 일이 중요해졌다.
표준이 하는 일
프로그래밍 언어의 표준은 해당 언어가 어떤 문법과 규칙을 가지는지 정의한다. 표준 라이브러리가 제공해야 하는 기능과 각 기능이 어떤 방식으로 동작해야 하는지도 함께 정한다.
컴파일러 개발자는 이 기준을 바탕으로 언어를 구현하고, 개발자는 특정 컴파일러의 동작이 아닌 표준에 정의된 규칙을 기준으로 코드를 작성할 수 있다.
물론 표준이 존재한다고 해서 운영체제나 하드웨어에 따른 모든 차이가 사라지는 것은 아니다. 표준은 모든 환경을 완전히 동일하게 만드는 것이 아니라, 공통으로 지켜야 할 규칙과 구현에 맡겨지는 부분을 구분한다.
C++ 98의 등장
C++의 표준화 작업은 1989년 ANSI X3J16 위원회가 구성되면서 본격적으로 시작되었다. 이후 1991년부터는 국제 표준화를 위해 ISO WG21과 공동으로 작업이 진행되었다.
표준화 과정에서는 C++의 문법뿐만 아니라 실제 개발에 필요한 라이브러리와 기능도 함께 논의되었다. 당시의 주요 대상에는 입출력 스트림, 템플릿, 예외 처리, 네임스페이스, RTTI, STL 등이 포함되었다.
이 과정에서 기존 문서와 구현을 바탕으로 C++의 문법과 동작을 정리하고, 서로 다른 환경에서 공통으로 사용할 수 있는 규격을 만들어갔다.
그 결과 1998년, C++은 최초의 ISO 표준인 C++98을 갖게 되었다.
Template
Template은 특정 자료형에 종속되지 않는 코드를 작성하기 위한 표준 기능으로 정리되었다.
Template이 필요했던 이유는 여러 자료형에 동일한 로직을 적용하면서도, 코드의 재사용성과 타입 안정성을 함께 확보하기 위해서였다. 자료형마다 별도의 함수나 자료구조를 작성하면 중복 코드가 늘어나고, void*와 같은 방식으로 처리하면 컴파일러가 타입 오류를 충분히 확인하기 어려워진다.
template <typename T>
T Max(T a, T b)
{
return a > b ? a : b;
}
int main()
{
int number = Max(10, 20);
double value = Max(1.5, 0.5);
}예시의 Max() 함수는 특정 자료형에 종속되지 않으며, 호출할 때 전달된 자료형에 맞춰 사용할 수 있다. 이러한 방식은 이후 여러 자료형을 다루는 컨테이너와 알고리즘을 작성하는 기반이 되었다.
특히 1993년 Alex Stepanov가 제네릭 프로그래밍을 소개한 이후, 템플릿을 활용한 STL이 표준 라이브러리의 중요한 후보로 논의되기 시작했다. STL은 템플릿을 기반으로 여러 자료형에서 동작하는 컨테이너와 알고리즘을 제공했으며, 결국 C++98 표준 라이브러리의 핵심으로 자리 잡았다.
Template은 단순히 함수를 편리하게 재사용하기 위한 문법이 아니라, C++이 표준 라이브러리를 구축하고, 자료형에 독립적인 코드를 작성하는 언어로 발전하기 위해 필요한 기반이었다.
STL
템플릿을 통해 여러 자료형에 적용할 수 있는 코드를 작성할 수 있게 되었지만, 자료구조와 알고리즘을 개발자가 매번 직접 구현해야 한다는 문제는 여전히 남아 있었다.
C++98 표준화 과정에서는 이러한 문제를 해결하기 위해 여러 자료구조와 알고리즘을 재사용할 수 있는 표준 라이브러리를 정리했다. 이것이 바로 STL(Standard Template Library)이다.
STL은 크게 다음 요소로 구성된다.
Container: 데이터를 저장한다.Algorithm: 저장된 데이터를 처리한다.Iterator:Container의 요소에 접근하고 순회하는 공통 규약을 제공하여Container와Algorithm을 연결한다.
예를 들어 std::vector는 데이터를 저장하는 Container이고, std::sort는 데이터를 정렬하는 Algorithm이다.
#include <algorithm>
#include <vector>
int main()
{
std::vector<int> numbers;
numbers.push_back(30);
numbers.push_back(10);
numbers.push_back(20);
std::sort(numbers.begin(), numbers.end());
}std::sort()는 std::vector의 내부 구조를 직접 알지 않아도 Iterator를 통해 데이터에 접근한다. 따라서 데이터를 저장하는 방식과 데이터를 처리하는 로직을 서로 분리할 수 있다.
이러한 구조를 통해 개발자는 연결 리스트나 정렬 알고리즘을 직접 구현하지 않고도 검증된 기능을 재사용할 수 있게 되었다. 또한 템플릿을 기반으로 작성되었기 때문에 특정 자료형에 종속되지 않고 다양한 자료형에 사용할 수 있게 되었다.
Exception Handling
이전에는 함수가 실패했을 때 반환 값이나 별도의 상태 값을 통해 오류를 알리는 방식이 사용되어 왔었다. 하지만 프로그램의 규모가 커지면 모든 호출자가 반환 값을 확인해야 하며, 정상적인 처리 로직과 오류 처리 로직이 복잡하게 문제가 생길 수 있었다.
C++98 표준화 과정에서는 이러한 오류 처리 방식을 언어 차원에서 정리하기 위해 예외 처리 기능을 표준의 일부로 포함했다. 예외 처리는 오류가 발생한 위치와 오류를 처리하는 위치를 분리할 수 있도록 한다.
일반적으로 try, throw, catch를 사용해 예외를 처리한다.
#include <stdexcept>
int Divide(int a, int b)
{
if (b == 0)
{
throw std::runtime_error("0으로 나눌 수 없다.");
}
return a / b;
}
int main()
{
try
{
int result = Divide(10, 0);
}
catch (const std::exception& e)
{
// 예외 처리
}
}Divide() 함수는 나눌 수 없는 상황을 만나면 throw를 통해 예외를 발생시킨다. 예외가 발생하면 현재 함수의 실행이 중단되고, 호출 스택을 거슬러 올라가 해당 예외를 처리할 수 있는 catch 블록을 찾는다.
이를 통해 정상적인 계산 로직과 오류 처리 로직을 분리할 수 있게 된다.
더 나아가기
여기서부터는 앞에서 배운 개념을 다른 관점으로 확장해본다.
중요한 것은 정답을 맞히는 것이 아니라, 지금까지 살펴본 개념을 바탕으로 충분히 고민하고 자신의 생각을 논리적으로 정리하여 확장해보는 것이다.
STL의Iterator는 객체지향 프로그래밍의 어떤 특성과 연결되며,Template을 이용한 다형성과는 어떤 관계가 있을까?
나만의 대답
STL의 구조는 Container, Iterator, Algorithm이 각각 다른 책임을 나누어 가진다는 점에서 객체지향 프로그래밍의 설계 원리와 연결할 수 있다.
먼저 Container는 데이터를 저장하고 관리하는 책임을 가진다. std::vector가 내부적으로 배열을 사용하는지, std::list가 노드와 포인터를 사용하는지는 사용하는 쪽에서 직접 알 필요가 없다. 데이터의 저장 방식과 관리 방법을 객체 내부에 숨긴다는 점에서 Container는 캡슐화와 연결된다.
Iterator는 컨테이너의 내부 구조를 직접 노출하지 않고, 요소에 접근하고 다음 요소로 이동하는 데 필요한 연산만 제공한다. 사용하는 쪽은 데이터가 연속적으로 저장되었는지, 노드로 연결되어 있는지 알지 못해도 *, ++, !=과 같은 공통된 연산을 사용할 수 있다. 복잡한 내부 구현에서 필요한 기능만 외부에 제공한다는 점에서 Iterator는 추상화와 연결된다.
C++98 표준에는Namespace가 왜 필요했을까?
나만의 대답
C++이 발전하고 프로그램과 라이브러리의 규모가 커지면서 전역 공간에 선언되는 이름이 많아졌다. 따라서 여러 라이브러리를 함께 사용하면 서로 다른 개발자가 같은 이름의 함수나 클래스, 변수 등을 선언할 가능성이 커졌다. 이때 이름이 충돌하면 어떤 대상을 사용해야 하는지 구분하기 어려워지고, 라이브러리 코드를 함께 사용하는 것도 복잡해 질 수 있다.
Namespace는 이름을 특정 영역 안에 소속시켜 이러한 충돌을 줄이기 위해 만들어졌다. 예를 들어 C++ 표준 라이브러리의 기능은 std라는 Namespace 안에 정의되어 있으므로, std::vector와 같이 이름의 소속을 명확하게 표현할 수 있다.
Template을 이용한 컴파일 타임 다형성과Virtual Function을 이용한 런타임 다형성은 어떤 차이가 있을까?
나만의 대답
Template을 이용한 컴파일 타임 다형성은 컴파일 과정에서 사용할 자료형과 호출할 함수가 결정된다.
컴파일러는 Template을 사용하는 자료형에 맞춰 필요한 코드를 생성하므로, 일반적으로 실행 중에 객체의 타입을 확인하거나 가상 함수를 통해 호출 대상을 결정하는 과정이 필요하지 않다.
반면 Virtual Function을 이용한 런타임 다형성은 기반 클래스의 포인터나 참조를 통해 객체를 다룰 때 사용된다. 이 경우 실제 객체의 타입에 따라 실행 중에 호출할 함수가 결정된다.
| 구분 | Template 기반 다형성 | Virtual Function 기반 다형성 |
|---|---|---|
| 결정 시점 | 컴파일 타임 | 런타임 |
| 주요 기반 | Template과 자료형 | 상속과 기반 클래스 |
| 호출 대상 | 컴파일 과정에서 결정 | 실행 중 실제 객체에 따라 결정 |
| 장점 | 동적 디스패치 없이 타입에 맞는 코드 생성 | 서로 다른 객체를 공통 인터페이스로 처리 |
| 한계 | 자료형마다 코드가 생성될 수 있음 | 상속 구조와 가상 함수 호출 비용이 필요할 수 있음 |