C에서 코드 복제가 필요한 악입니까?


16

나는 C에 익숙하지 않고 일반적인 데이터 구조와 C를 작성할 때 코드 복제가 필요한 악한 것인지 궁금합니다.

hash map예를 들어 일반적인 구현을 작성할 수는 있지만 항상 최종 결과가 지저분하다는 것을 알고 있습니다. 또한이 특정 사용 사례에 맞게 특수 구현을 작성할 수 있으며 코드를 명확하고 읽기 쉽고 디버그 할 수 있습니다. 후자는 물론 일부 코드 복제로 이어질 것입니다.

일반적인 구현입니까, 아니면 각 사용 사례마다 다른 구현을 작성합니까?


11
당신이 묘사하는 도덕적 규범은 C에게 독특하지 않습니다. 우수한 일반 구현을 작성하는 데 필요한 노력은 point-solution, IME를 훨씬 능가합니다.
Robert Harvey


1
@ kevincline : 글쎄, Java에는 제네릭이 있습니다. 물론, 그것은 "커튼 뒤에있는 사람에게주의를 기울이지 마십시오"유형의 제네릭입니다.
Robert Harvey

1
그리고 제네릭 이전에는 Object를 전달하고 Float, Double, Integer 및 Long을 각각 전달할 수있었습니다.
Daniel Kaplan

3
Deja vu는 코드 냄새 검사이며 DRY에 대한 선호는 IMO에주의를 기울일 가치가있는 모든 모범 사례의 근본입니다. 그러나 나는 OOP 지향 언어로 발을 들여 스스로를 복제하지 않기 위해 너무 열심히 노력했습니다. 관계없는 관계를 불필요하게 서로 구속하지 않거나 두 개의 유사한 기능이 하나가되어 가독성을 파괴하지 않는다면 대부분의 언어로 본능에 따라 갈 것입니다.
Erik Reppen

답변:


27

C는 일반 코드 작성을 어렵게합니다. 템플릿과 가상 함수를 제공하는 C ++과 달리 C에는 일반 코드 작성을위한 3 가지 메커니즘 만 있습니다.

  1. void* 포인터
  2. 전 처리기 매크로
  3. 함수 포인터

void* 컴파일러가 제공하는 모든 유형 안전을 잃어 버리기 때문에 포인터가 이상적이지 않습니다. 이로 인해 유효하지 않은 유형 캐스트로 인해 디버그가 정의되지 않은 동작이 발생할 수 있습니다.

전 처리기 매크로에는 잘 알려진 단점이 있습니다. 전 처리기 확장은 기본적으로 컴파일 단계 이전에 발생하는 찾기 / 바꾸기 메커니즘 일 뿐이며, 다시 디버깅하기 어려운 오류가 발생할 수 있습니다. 전형적인 예는 다음과 같습니다. #define add(x) (x+x), x호출하면 두 번 증가 할 수 있습니다 add(i++). 당신은 할 수 완전히 C-매크로를 사용하여 템플릿 스타일의 일반적인 코드를 작성하지만, 결과는 정말 끔찍한 및 유지 보수가 어렵다 .

함수 포인터는 일반 코드를 작성하는 좋은 방법을 제공하지만 불행히도 형식 일반성을 제공하지는 않습니다. 런타임 다형성의 가능성 만 제공합니다 (예를 들어 표준 라이브러리 qsort에는 여전히 필요한 함수가 필요합니다) void*포인터.)

일반 GObject기본 클래스 를 제공하는 GLib 라이브러리에서와 같이 구조체를 사용하여 C에서 클래스 계층을 구현할 수도 있습니다 . 그러나 void*잠재적으로 안전하지 않은 수동 캐스팅을 사용하여 업 캐스트 및 다운 캐스트해야하므로 포인터 를 사용할 때와 비슷한 문제가 발생 합니다.

따라서 C는 일반적이고 안전하고 유지 관리하기 쉬운 코드를 작성하기가 어렵습니다. 불행히도 코드 복제가 발생할 수 있습니다. 대규모 C 프로젝트는 종종 스크립팅 언어를 사용하여 빌드 프로세스 중에 반복 코드를 생성합니다.


1
외부 템플릿 언어 / 도구 또는 쉘 스크립트를 사용하거나 개미 또는 ...를 사용하여 반복적 인 C 코드를 생성 할 수도 있습니다.
Job

귀하의 답변에 입력 한 Google의 난독 화 된 러시아어 URL은 무엇입니까? 클릭하면 브라우저에 "열기"프롬프트가 나타납니다. 안전한가요?
Robert Harvey

2
@RobertHarvey 내 바이러스 백신 및 보안 검색 결과에 문제가 없다고 생각합니다. C 언어 헤더 파일이며 확장자는 .h입니다. 합법적으로 보입니다.
maple_shaft

@maple_shaft : 좋습니다. 링크 주변에서 인터넷 검색을 제거했습니다.
Robert Harvey

그냥 일반 텍스트 .H (C 헤더) 파일입니다
찰스 샐비어

15

나는 다른 사람들을 위해 말할 수 없지만 C에 대한 개인적인 경험에서 코드 복제는 큰 문제가되지 않았습니다. 이것이 프로젝트 크기 때문인지, 매력적인 샘플 세트인지 여부는 말할 수 없습니다. 그러나 내가 적용 할 수 있다고 생각하는 세 가지 규칙이 있습니다. 특별한 순서없이, 그들은 ...

  1. 필요한 것을 쓰십시오. 필요한 경우 일반성은 나중에 올 수 있습니다.
  2. 일반성이 필요한 경우, void 포인터, 함수 포인터 및 구조 크기는 매우 중요합니다. 예를 들어, qsort () 루틴은 세 가지를 모두 사용합니다.
  3. 코드를 명확하게 만드는 데 중점을 둡니다.

5

코드 복제를 줄이기 위해 완전히 일반적인 솔루션이 필요하지 않을 수도 있습니다. 때때로 일반적인 리팩토링과 약간의 메소드 일반화로 충분합니다.

광범위한 청중을위한 일반화 된 솔루션을 작성할 때는 다음을 고려해야합니다.

  1. 소비자는 어떤 새로운 방법으로 코드를 사용할 수 있으며 어떻게 코드를 수용합니까?
  2. 어떤 에러를 잡아야합니까? 어떻게 오류를해야 하지 잡기?
  3. API는 얼마나 강력해야합니까? 얼마나 많은 과부하를 제공해야합니까?
  4. 코드를 악의적 인 목적으로 사용할 수 없도록 어떤 보안 조치를 취해야합니까?

Eric Lippert .NET 프레임 워크 코드에서 단일 디자인 결정에 대한 전체 기사를 작성했습니다 . 대체로 리팩토링하는 것이 더 간단한 경우가 많습니다.


3

나는 일반적으로 glib 's 와 같은 일반적인 구현을 사용하고 , 캐스팅이 너무 성가신 경우 작은 유형별 래퍼를 만듭니다. 그러나 void*일반적인 유형으로 a를 사용하는 것처럼 C에서는 많은 캐스팅이 예상 되므로 다른 언어에서 "지저분한"것으로 간주되는 것은 일반적인 C입니다. 언어에 대한 더 많은 경험을 얻습니다.


2
"다른 언어로 '지저분한'것으로 간주되는 것은 단지 전형적인 C입니다. 거의 언어에 대한 경험이 많을수록 훨씬 더 자연스럽게 보일 것입니다." 하하하! C는 항상 내가 가장 좋아하는 언어 중 하나 였지만 여전히 꽤 재미 있습니다!
GlenPeterson

0

재사용 성 문제는 C만의 고유 한 문제는 아니지만 재사용이 가능한 우수하고 재사용 가능한 HashMap 구현이 있습니다. 10 ~ 2 년 동안 있었던 것 중 하나는 Java입니다 : http://docs.oracle.com/javase/7/docs/api/java/util/HashMap.html

HashMap을 포함한 모든 Java 콜렉션은 반복기의 아이디어를 없애기 위해 Java 8에 대해 다시 작성되고 대신 내부적으로 반복 할 수 있도록 함수를 콜렉션에 전달해야합니다. 이것은 동시 코드를 작성하는 데 큰 도움이됩니다. 컬렉션은 동시성을 내부적으로 관리 할 수 ​​있으므로 클라이언트가 할 필요가 없습니다. 즉, 대부분의 프로그래머는 함수 포인터를 전달하는 데 익숙하지 않습니다. 이것은 HashMap이 향하는 곳을 잘 설명합니다 : http://cr.openjdk.java.net/~briangoetz/lambda/collections-overview.html

스칼라의 구문은 이와 같은 것을 C로 번역하기 어려울 수 있지만 여기에 Java 8 콜렉션이 작동하는 방식으로 작동하는 콜렉션의 예가 있습니다. 기본적으로 불변이며 다양한 변환 방법을 제공하고 함수 포인터 및 기타를 사용합니다. 부분 응용 프로그램과 같은 멋진 것들 : http://www.scala-lang.org/api/current/index.html#scala.collection.Map

HashMaps 및 기타 고급 데이터 구조로 많은 작업을 수행하는 경우이를 지원하는 언어를 살펴볼 수 있습니다.

일반적으로 코드 복제라는 개념으로 돌아가서 잘라내어 붙여 넣기를하면 실제로 코드를 빠르게 작성할 수 있지만 끔찍한 코드가 생성됩니다. 일회성 보고서와 같은 일회용 코드에서는 허용됩니다. 다운 스트림 코드가 없을 때도 괜찮습니다. UI 코드와 같이 아무것도 의존하지 않는 화면을 만들지 만 실제로는 매우 회색 영역입니다.

코드를 복제 할 때마다 기능을 만들어야한다고 말하는 것은 너무 단순화 된 것이라고 생각하지 않습니다. 하나의 코드 사본에서 수정하고 다른 코드는 수정하지 않는 버그만으로도 기능을 수행 할 수 있습니다.


1
- "코드를 복제 할 때마다 함수를 작성해야한다고 말하는 것은 너무 단순화 된 것이 아니라고 생각합니다."-C에서는 너무 간단합니다. 각 데이터 유형에 대해 push 및 pop 함수를 복제하지 않고 정수, 이중 및 문자열에 대해 C로 스택을 작성하십시오.
mike30

@mike void 포인터를 사용하여 가능하지 않습니까?
사키 스크

@ 마이크-좋은 지적. 그러나 반격은 스택을 다시 구현할 필요가없는 방식으로 각 데이터 유형에 대해 스택을 구현할 수 있다는 것입니다. 그래서 목표는 numberOfImplementations = 1이 아니라 5와 같습니다. 당신은 s에 대한 int구현 작업을 수행 할 수 있습니다 char... 기억할 수없는 C를 사용한 지 오래되었습니다.
GlenPeterson

1
@faif. 예, 그러나 void 포인터를 사용해야하므로 C의 요점이됩니다. 포인터를 캐스팅하는 것은 C ++에있는 대안보다 느리고 우아하지 않습니다. 코드를 복제하면 C로 코딩 할 때 가능한 옵션 목록에서 다소 높은 순위를 나타
냅니다

@ mike30 다음에 대한 증거 / 참조가 있습니까 : "C에있는 포인터가 C ++에있는 다른 것보다 느립니다."
sakisk

0

일반적인 데이터 구조와 일반적으로 C를 작성할 때 코드 복제가 필요한 악인지 궁금합니다.

C에서 C와 C ++ 사이에서 튀는 사람으로서 절대적으로 나를 위해. 나는 C ++보다 C에서 매일 더 사소한 것들을 복제하지만, 고의적으로, 적어도 실질적인 이점이 있기 때문에 반드시 "악"으로 볼 필요는 없다-모든 것을 고려하는 것은 실수라고 생각한다 엄밀히 말하면 "좋은 것"또는 "사악한 것"입니다. 이러한 절충점을 명확하게 이해하는 것은 후견에서 후회할 수없는 결정을 피하지 않는 열쇠이며, 단순히 "좋은"또는 "악한"것으로 표시하는 것은 일반적으로 그러한 모든 미묘함을 무시합니다.

다른 사람들이 지적한 것처럼 문제가 C에 고유하지는 않지만, 제네릭에 대한 매크로 나 void 포인터, 사소한 OOP의 어색함 및 그 사실에 따라 C에서 훨씬 더 악화 될 수 있습니다. C 표준 라이브러리에는 컨테이너가 제공되지 않습니다. C ++에서 자신의 링크 된 목록을 구현하는 사람은 학생이 아닌 한 표준 라이브러리를 사용하지 않는 이유를 요구하는 화난 사람들을 얻을 수 있습니다. C 프로그래머는 적어도 매일 그런 유형의 일을 할 수 있기 때문에 C에서, 수면에 우아한 링크드리스트 구현을 자신있게 구현할 수 없다면 화난 군중을 초대 할 것입니다. 그것' Linus Torvalds가 언어를 이해하고 "맛이 좋은"프로그래머를 평가하기위한 기준으로 이중 간접 지정을 사용하여 SLL 검색 및 제거 구현을 사용했다는 링크 된 목록에 대한 이상한 강박 관념 때문이 아닙니다. C 프로그래머는 커리어에서 이러한 논리를 수천 번 구현해야 할 수도 있기 때문입니다. 이 경우 C의 경우, 요리사가 항상 계란을 준비하여 최소한 항상 필요한 기본 사항을 숙달했는지 확인하여 새로운 요리사의 기술을 평가하는 요리사와 같습니다.

예를 들어,이 할당 전략을 사용하는 각 사이트에 대해 C에서 로컬로이 기본 "인덱싱 된 자유 목록"데이터 구조를 로컬로 수십 번 구현했을 것입니다. 64 비트 링크 비용) :

여기에 이미지 설명을 입력하십시오

그러나 C에서는 realloc확장 가능한 배열에 매우 적은 양의 코드를 가져 와서이를 사용하는 새로운 데이터 구조를 구현할 때 인덱스 된 접근 방식을 사용하여 여유 목록에 대한 메모리를 풀링합니다.

이제 C ++에서 동일한 것을 구현했으며 클래스 템플릿으로 한 번만 구현했습니다. 그러나 수백 줄의 코드와 수백 줄의 코드에 걸친 일부 외부 종속성으로 C ++ 측에서 훨씬 더 복잡한 구현입니다. 그리고 훨씬 더 복잡한 주된 이유는 T가능한 모든 데이터 유형이 될 수있는 아이디어에 대해 코드를 작성해야하기 때문 입니다. 주어진 시간에 던질 수 있습니다 (표준 라이브러리 컨테이너와 같이 명시 적으로 해야하는 것을 제외하고는 메모리를 할당하기 위해 적절한 정렬에 대해 생각해야했습니다)T (다행히도 C ++ 11부터는 더 쉬워 지지만) 사소하게 구성 가능 / 파괴 가능 할 수 있습니다 (새롭고 수동적 인 dtor 호출을 배치해야 함). 모든 것이 필요하지는 않지만 필요한 것이있는 메소드를 추가해야합니다. 그리고 가변 및 읽기 전용 (const) 반복자 등의 반복자를 추가해야합니다.

성장 가능한 배열은 로켓 과학이 아닙니다

C ++에서 사람들 std::vector은 로켓 과학자의 작품 처럼 들리지만 죽음에 최적화되어 있지만 특정 데이터 유형에 대해 코딩 된 동적 C 배열보다 성능이 좋지 않습니다 realloc. 수십 줄의 코드. 차이점은 확장 가능한 랜덤 액세스 시퀀스를 표준과 완벽하게 호환하고 삽입되지 않은 요소에 대해 ctor를 호출하지 않고 예외 안전하고 const 및 non-const 랜덤 액세스 반복자를 제공하며 유형을 사용하려면 매우 복잡한 구현이 필요하다는 것입니다 특정 정수 유형에 대해 범위 ctor에서 채우기 ctor를 명확하게하는 특성T, 유형 특성 등을 사용하여 POD를 다르게 처리 할 수 ​​있습니다. 그 시점에서 실제로 확장 가능한 동적 배열을 만들기 위해 매우 복잡한 구현이 필요하지만 가능한 모든 사용 사례를 상상할 수 있기 때문에 처리해야하기 때문입니다. 또한 POD와 사소한 UDT를 모두 저장하고 모든 호환 데이터 구조에서 작동하는 일반 반복자 기반 알고리즘을 사용하는 경우 추가 노력을 기울이면 많은 마일리지를 얻을 수 있습니다. 예외 처리 및 RAII의 이점, 적어도 때때로 std::allocator자신의 사용자 지정 할당 자 등으로 재정의 하십시오. 얼마나 많은 이점을 고려할 때 표준 라이브러리에서 확실히 지불합니다.std::vector 그것을 사용하는 사람들의 전 세계에 있었지만 그것은 전 세계의 요구를 목표로하도록 설계된 표준 라이브러리에서 구현 된 것입니다.

매우 구체적인 사용 사례를 처리하는 간단한 구현

내 "인덱싱 된 무료 목록"으로 매우 구체적인 사용 사례를 처리 한 결과,이 무료 목록을 C 측에서 수십 번 구현했지만 결과적으로 사소한 코드가 복제 되었음에도 불구하고 코드를 적게 작성했을 것입니다 C ++에서 C ++로 한 번만 구현하는 것보다 수십 번을 구현하는 데 총 C가 있었고, 하나의 C ++ 구현을 유지하는 것보다 수십 개의 C 구현을 유지하는 데 적은 시간을 소비했습니다. C 측이 너무 간단한 주요 이유 중 하나는이 기술을 사용할 때마다 일반적으로 C에서 POD로 작업하고 일반적으로 insert및 보다 더 많은 기능이 필요하지 않기 때문입니다.erase로컬로 구현하는 특정 사이트에서 기본적으로 나는 C ++ 버전이 제공하는 기능 중 가장 작은 부분 집합을 구현할 수 있습니다. 왜냐하면 매우 구체적인 용도로 디자인을 구현할 때 디자인에 필요하지 않은 것에 대해 더 많은 가정을 자유롭게 할 수 있기 때문입니다. 케이스.

이제 C ++ 버전은 사용하기에 훨씬 더 좋고 형식이 안전하지만, 예외적이고 양방향 반복기 호환을 구현하고 구현하는 것이 여전히 중요한 PITA였습니다. 이 경우 실제로 절약하는 것보다 많은 시간이 걸립니다. 그리고 일반화 된 방식으로 구현하는 데 드는 많은 비용은 선불뿐만 아니라 매일 반복적으로 지불되는 빌드 시간과 같은 형태로 반복적으로 낭비됩니다.

C ++에 대한 공격이 아닙니다!

그러나 이것은 C ++을 좋아하기 때문에 C ++에 대한 공격은 아니지만 데이터 구조와 관련하여 구현하기 위해 훨씬 더 많은 시간을 투자하고 싶은 사소한 데이터 구조에 대해 C ++을 선호하게되었습니다. 매우 일반적인 방식으로, 가능한 모든 유형에 대해 예외 안전을 보장하고 T, 표준을 준수하고 반복 가능 하게 만드는 등의 선행 비용 유형이 실제로 마일리지의 형태로 지불됩니다.

그러나 이는 또한 매우 다른 디자인 사고 방식을 장려합니다. C ++에서 충돌 감지를 위해 Octree를 만들고 싶다면 n도까지 일반화하려는 경향이 있습니다. 인덱스 삼각형 메쉬를 저장하고 싶지 않습니다. 런타임에 모든 추상화 페널티를 제거하는 매우 강력한 코드 생성 메커니즘을 손끝에서 사용할 수있는 하나의 데이터 유형으로 제한해야하는 이유는 무엇입니까? 절차적인 구체, 큐브, 복셀, NURB 표면, 점 구름 등을 저장하고 손끝에 템플릿이있을 때 디자인하려는 유혹 때문에 모든 것에 적합하게 만들려고합니다. 충돌 감지로 제한하고 싶지 않을 수도 있습니다. 레이 트레이싱, 피킹 등은 어떻습니까? C ++로 처음에는 "정말 쉬워"보입니다 데이터 구조를 n 도로 일반화합니다. 이것이 C ++에서 이러한 공간 인덱스를 디자인하는 데 사용한 방법입니다. 나는 전 세계의 기아 요구를 처리하기 위해 그것들을 설계하려고 노력했으며, 내가 교환 한 것은 일반적으로 상상할 수있는 모든 사용 사례와 균형을 맞추기 위해 매우 복잡한 코드를 가진 "모든 거래의 잭"이었습니다.

그러나 재미있게도, 나는 수년 동안 C로 구현 한 공간 인덱스에서 더 많은 재사용을 얻었으며 C ++의 결함은 없지만 언어가 저를 유혹하는 일에서만 내 재사용을 얻었습니다. C에서 octree와 같은 코드를 작성할 때, 언어로 포인트를 일반화하기가 너무 어려워서 포인트로만 작동하고 만족하게 만드는 경향이 있습니다. 그러나 이러한 경향으로 인해 수년에 걸쳐 실제로 더 효율적이고 신뢰할 수 있으며 실제로 특정 작업에 적합한 제품을 디자인하는 경향이 있습니다. 그들은 모든 거래의 잭 대신 하나의 전문 카테고리에서 에이스가됩니다. 다시 말하지만 C ++의 결함은 아니지만 C와 달리 사용할 때 인간의 경향이 있습니다.

어쨌든, 나는 두 언어를 모두 좋아하지만 다른 경향이 있습니다. CI에서는 충분히 일반화하지 않는 경향이 있습니다. C ++에서는 너무 많이 일반화하는 경향이 있습니다. 두 가지를 모두 사용하면 균형을 잡는 데 도움이되었습니다.

일반적인 구현입니까, 아니면 각 사용 사례마다 다른 구현을 작성합니까?

배열의 노드 또는 자체를 재 할당하는 배열 ( std::vectorC ++에서 와 동등한)을 사용하는 단일 링크 된 32 비트 인덱스 목록 또는 포인트를 저장하고 더 이상 아무것도하지 않는 옥트리 와 같은 사소한 일 의 경우 데이터 형식을 저장하는 지점까지 일반화하지 않아도됩니다. 특정 데이터 유형을 저장하기 위해 이들을 구현합니다 (추상적 일 수 있지만 정적 다형성으로 오리 타이핑보다 더 구체적인 함수 포인터를 사용할 수는 있지만).

그리고 나는 그 경우에 중복 약간 완벽하게 행복 해요 하여 제공된 것을 내가 단위 테스트 그것을 철저하게. 단위 테스트를하지 않으면 실수를 복제 할 수있는 중복 코드가있을 수 있기 때문에 중복성이 훨씬 더 불편 해지기 시작합니다. 예를 들어 작성중인 코드 유형에 따라 설계 변경이 필요하지 않더라도, 손상되었으므로 여전히 변경이 필요할 수 있습니다. 나는 이유로 작성한 C 코드에 대해 더 철저한 단위 테스트를 작성하는 경향이 있습니다.

사소한 일이 아닌 경우, 일반적으로 C ++에 도달 할 때이지만 C로 구현하려는 경우 void*포인터 만 사용 하고 각 요소에 할당 할 메모리 양을 알기 위해 유형 크기를 수락하고 copy/destroy함수 포인터를 고려할 수 있습니다 사소한 구성 / 파괴가 불가능한 경우 데이터를 딥 카피 및 파기합니다. 대부분의 경우 가장 복잡한 데이터 구조와 알고리즘을 만들기 위해 C를 많이 사용하지 않고 귀찮게하지 않습니다.

특정 데이터 형식으로 하나의 데이터 구조를 충분히 자주 사용하는 경우 비트 및 바이트 및 함수 포인터와 함께 작동하는 형식 안전 버전과 void*형식 래프 버전을 래핑 할 수 있습니다 ( 예 : C 래퍼를 통해 형식 안전을 다시 설정).

예를 들어 해시 맵에 대한 일반적인 구현을 시도 할 수는 있지만 항상 최종 결과가 지저분 해집니다. 또한이 특정 사용 사례에 맞게 특수 구현을 작성할 수 있으며 코드를 명확하고 읽기 쉽고 디버그 할 수 있습니다. 후자는 물론 일부 코드 복제로 이어질 것입니다.

해시 테이블은 해시, 리해시와 관련하여 요구가 얼마나 복잡한 지에 따라 구현하기가 쉽지 않거나 실제로 복잡 할 수 있기 때문에 일종의 iffy입니다. 테이블이 암시 적으로 자동으로 커지거나 테이블 크기를 예상 할 수있는 경우 개방형 주소 지정을 사용하든 별도의 체인을 사용하든 상관없이 진행해야합니다. 그러나 명심해야 할 것은 해시 테이블을 특정 사이트의 요구에 완벽하게 맞출 경우 구현이 그렇게 복잡하지 않고 종종 승리한다는 것입니다 이러한 요구에 정확하게 맞출 때 너무 중복되지 않습니다. 적어도 그것이 현지에서 무언가를 구현하면 나에게주는 변명입니다. 그렇지 않다면 위에서 설명한 방법 void*과 함수 포인터를 사용하여 사물을 복사 / 파괴하고 일반화 할 수 있습니다.

대안이 정확한 사용 사례에 매우 좁게 적용되는 경우 매우 일반화 된 데이터 구조를이기는 데 많은 노력이나 코드가 필요하지 않은 경우가 종종 있습니다 . 예를 들어 malloc(각 노드마다 많은 메모리를 풀링하는 것과는 달리) 각 노드마다 사용하는 성능을 한 번만 능가하고 코드를 사용하여 매우 정확한 유스 케이스를 다시 방문 할 필요가 없습니다. 새로운 구현이 malloc나오 더라도 . 그것을 극복하고 평범함과 일치 시키려면 인생의 큰 부분을 유지하고 최신 상태로 유지하는 데 코드를 작성하는 데 평생이 걸릴 수 있습니다.

다른 예로, 필자는 Pixar 또는 Dreamworks에서 제공하는 VFX 솔루션보다 10 배 이상 빠른 솔루션을 구현하는 것이 매우 쉬운 경우가 종종 있음을 발견했습니다. 잠을 잘 수 있습니다. 그러나 그것은 내 구현이 훨씬 멀리 떨어져 있기 때문이 아닙니다. 대부분의 사람들에게는 열등합니다. 그들은 매우 구체적인 사용 사례에 대해서만 우수합니다. 내 버전은 Pixar 또는 Dreamwork보다 훨씬 덜 일반적으로 적용됩니다. 그들의 솔루션이 내 바보 같은 간단한 솔루션에 비해 절대적으로 훌륭하기 때문에 엄청나게 불공평 한 비교이지만, 그 점이 중요합니다. 비교는 공정하지 않아도됩니다. 필요한 것이 몇 가지 매우 구체적인 것이라면 데이터 구조가 필요없는 끝없는 목록을 처리하도록 할 필요는 없습니다.

동종 비트 및 바이트

C 에서 유형 안전이 본질적으로 부족 하기 때문에 C에서 악용해야 할 한 가지는 비트와 바이트의 특성에 따라 균등하게 저장하는 아이디어입니다. 메모리 할당 자와 데이터 구조 사이에 결과가 더 흐려집니다.

그러나 가변 크기의 물건, 또는 단순히 심지어 물건을 잔뜩 저장 할 수 다형성처럼, 가변 크기의 수 Dog등을 Cat효율적으로 수행하기가 어렵습니다. 한 요소에서 다른 요소로가는 걸음이 다를 수 있기 때문에 변수 크기가 가변적이고 간단한 랜덤 액세스 컨테이너에 연속적으로 저장할 수 있다고 가정 할 수는 없습니다. 개와 고양이가 모두 포함 된 목록을 저장하려면 3 개의 개별 데이터 구조 / 할당 자 인스턴스 (개, 고양이, 하나는 기본 포인터 또는 스마트 포인터의 다형성 목록 또는 더 나쁜 경우)를 사용해야합니다. , 각 개와 고양이를 범용 할당 자에 할당하고 메모리 전체에 뿌립니다). 이는 비싸고 멀티 캐시 캐시 미스가 발생합니다.

따라서 C에서 활용하는 한 가지 전략은 유형이 풍부하고 안전성이 떨어지지 만 비트 및 바이트 수준에서 일반화하는 것입니다. 당신은 가정 할 수 있습니다 DogsCats비트와 바이트의 같은 번호가 필요합니다, 같은 필드, 함수 포인터 테이블에 같은 포인터를 가지고있다. 그러나 그 대가로 적은 수의 데이터 구조를 코딩 할 수 있지만, 중요하게도이 모든 것들을 효율적이고 연속적으로 저장할 수 있습니다. 이 경우 개와 고양이를 유추 한 노조처럼 취급하고 있습니다 (또는 실제로 노조를 사용할 수도 있습니다).

그리고 그것은 안전을 입력하는 데 엄청난 비용이 듭니다. C에서 다른 것보다 놓친 것이 있다면 유형 안전성입니다. 구조가 메모리 할당량과 각 데이터 필드가 정렬되는 방식을 나타내는 어셈블리 수준에 가까워지고 있습니다. 그러나 이것이 실제로 C를 사용하는 가장 큰 이유 중 하나입니다. 실제로 메모리 레이아웃을 제어하려고 시도하고 모든 것이 할당되는 위치와 모든 것이 상대적으로 저장되는 위치는 종종 비트 수준에서 사물에 대해 생각하는 데 도움이됩니다. 바이트, 특정 문제를 해결하는 데 필요한 비트 및 바이트 수 C 타입 시스템의 멍청함은 실제로 핸디캡이 아니라 실제로 유익 할 수 있습니다. 일반적으로 결과적으로 처리 할 데이터 유형이 훨씬 줄어 듭니다.

환상 / 명백한 복제

이제 중복되지 않을 수도있는 것들에 대해 느슨한 의미로 "중복"을 사용하고 있습니다. 나는 사람들이 "사고 / 명백한"복제와 같은 용어를 "실제 복제"와 구별하는 것을 보았습니다. 내가 보는 방식은 많은 경우에 분명한 구별이 없다는 것입니다. 나는 "잠재적 독창성"과 "잠재적 복제"와 같은 구별을 발견하고 어느 쪽이든 갈 수 있습니다. 설계와 구현이 어떻게 발전하길 원하는지, 특정 사용 사례에 맞게 완벽하게 조정되는 방식에 따라 달라집니다. 그러나 종종 코드 중복으로 보이는 것이 나중에 여러 번 개선 된 후에 중복되지 않는 것으로 판명되었습니다.

를 사용하여 realloc유사한 확장 가능한 배열 구현을 사용 하십시오 std::vector<int>. 처음에는 std::vector<int>C ++에서 사용하여 중복 될 수 있습니다 . 그러나 측정을 통해 힙 할당을 요구하지 않고 16 개의 32 비트 정수를 삽입 할 수 있도록 64 바이트를 미리 할당하는 것이 유리할 수 있습니다. 이제는 더 이상 중복되지 않습니다 std::vector<int>. "하지만 이것을 이것을 새로운 것으로 일반화 SmallVector<int, 16>할 수 있습니다. 그러나 여러분은 그것이 매우 작은 짧은 수명의 어레이가 힙 할당에서 어레이 용량을 4 배로 늘리기 때문에 유용하다는 것을 알 수 있습니다. 1.5 증가vector어레이 용량이 항상 2의 거듭 제곱이라는 가정을 수행하면서 구현 사용). 이제 컨테이너가 실제로 다르며 컨테이너가 없을 수도 있습니다. 그리고 사전 할당 중화, 재 할당 동작 등을 정의하기 위해 점점 더 많은 템플릿 매개 변수를 추가하여 이러한 동작을 일반화하려고 시도 할 수는 있지만 그 시점에서 간단한 C 라인의 수십 줄에 비해 사용하기 어려운 것을 찾을 수 있습니다 암호.

또한 256 비트 정렬 및 패딩 된 메모리를 할당하고 AVX 256 명령어 전용 POD를 저장하고 128 바이트를 사전 할당하여 일반적인 경우 작은 입력 크기에 대한 힙 할당을 피하고 용량이 두 배가되는 데이터 구조가 필요한 시점에 도달 할 수도 있습니다. 배열 크기를 초과하지만 배열 용량을 초과하지 않는 후행 요소를 안전하게 덮어 쓸 수 있습니다. 이 시점에서 소량의 C 코드 복제를 피하기 위해 솔루션을 일반화하려고 시도하는 경우 프로그래밍 신이 당신의 영혼에 자비를 줄 수 있습니다.

따라서 특정 유스 케이스에 더 적합하고 더 나은 솔루션을 맞춤 구성하여 전혀 독창적이지 않고 전혀 중복되지 않는 솔루션에 맞출 때 처음에 중복으로 보이는 것이 점점 커지기 시작하는 시간도 있습니다. 그러나 특정 유스 케이스에 완벽하게 맞출 수있는 것들에만 해당됩니다. 때때로 우리는 우리의 목적을 위해 일반화 된 "괜찮은"것이 필요하며, 매우 일반화 된 데이터 구조로부터 가장 큰 이점을 얻을 수 있습니다. 그러나 특정 사용 사례를 위해 완벽하게 만들어진 예외적 인 경우 "일반적인 목적"과 "나의 목적을 위해 완벽하게 만들어진"이라는 개념은 너무 양립 할 수 없게됩니다.

포드 및 프리미티브

이제 C에서는 POD 및 특히 프리미티브를 가능할 때마다 데이터 구조에 저장하는 변명을 종종 발견합니다. 그것은 반 패턴처럼 보일지 모르지만 실제로는 C ++에서 더 자주 사용했던 것들에 비해 코드의 유지 관리 성을 향상시키는 데 실수로 도움이되는 것으로 나타났습니다.

간단한 예는 짧은 문자열을 인턴하는 것입니다 (일반적으로 검색 키에 사용되는 문자열의 경우와 같이 매우 짧은 경향이 있음). 런타임에 크기가 다른 모든 가변 길이 문자열을 처리하는 것이 귀찮은 이유는 무엇입니까? 문자열 인터 닝을 위해 설계된 스레드 안전 트리 또는 해시 테이블과 같은 중앙 데이터 구조에 이러한 것들을 저장 한 다음 평범한 이전 int32_t또는 다음 문자열을 참조하십시오 .

struct IternedString 
{
    int32_t index;
};

... 사전 식 정렬이 필요하지 않은 경우 해시 테이블, 빨강 검정 트리, 건너 뛰기 목록 등에서? 이제 우리가 32 비트 정수로 작업하도록 코딩 한 다른 모든 데이터 구조는 이제 32 비트 인이 인터 턴 된 문자열 키를 저장할 수 있습니다 ints. 그리고 적어도 유스 케이스에서 봤습니다 (레이 트레이싱, 메쉬 처리, 이미지 처리, 입자 시스템, 스크립팅 언어 바인딩, 저수준 멀티 스레드 GUI 키트 구현 등의 영역에서 일하기 때문에 도메인 일 수 있습니다. (낮은 수준이지만 OS만큼 낮은 수준은 아님) 코드가 우연히 이와 같은 것에 색인을 저장하는 것보다 더 효율적이고 간단 해집니다. 즉 나는 종종 단지로, 작업 시간의 75 %를 말하는거야 있도록하게 int32_t하고float32 내 사소한 데이터 구조에서 또는 같은 크기 (거의 항상 32 비트)를 저장합니다.

그리고 당연히 귀하의 경우에 적용 가능한 경우, 처음에는 거의 작업하지 않기 때문에 다양한 데이터 유형에 대해 여러 가지 데이터 구조 구현을 피할 수 있습니다.

테스트 및 신뢰성

내가 제공 할 마지막 것 중 하나는 모든 사람에게 해당되지 않을 수 있지만 해당 데이터 구조에 대한 테스트 작성을 선호하는 것입니다. 그것들을 정말 잘하게 만드십시오. 그들이 매우 신뢰할 수 있는지 확인하십시오.

이 경우 중복 된 코드를 계단식으로 변경해야하는 경우 코드 중복은 유지 관리 부담이므로 일부 사소한 코드 중복은 훨씬 더 용서할 수 있습니다. 이러한 중복 코드가 변경되는 주된 이유 중 하나는 코드의 안정성이 뛰어나고 수행하려는 작업에 매우 적합하다는 것입니다.

미학에 대한 나의 감각은 수년에 걸쳐 변했습니다. 한 라이브러리가 내적을 구현하거나 다른 라이브러리에서 이미 구현 된 사소한 SLL 논리를보고 있기 때문에 더 이상 짜증을 내지 않습니다. 일이 제대로 테스트되지 않고 신뢰할 수 없을 때만 짜증을 내며 훨씬 생산적인 사고 방식을 발견했습니다. 필자는 복제 코드를 통해 버그를 복제하는 코드 기반을 진정으로 다루었으며, 한 장소에서 사소한 변경을해야하는 최악의 복사 및 붙여 넣기 코딩 사례를 보았을 때 오류가 발생하기 쉬운 캐스케이드 변경이 많은 것으로 나타났습니다. 그러나 그 중 많은 시간은 테스트가 제대로 이루어지지 않았고 코드가 신뢰할 수 있고 코드가 처음부터 잘 수행되지 못한 결과였습니다. 버그가있는 레거시 코드베이스에서 작업하기 전에 제 생각에는 모든 형태의 코드 복제가 버그를 복제 할 가능성이 매우 높고 계단식 변경이 필요한 것으로 생각했습니다. 그럼에도 불구하고 한 가지 일을 매우 확실하고 확실하게 수행하는 미니어처 라이브러리는 여기 저기 중복되는 코드가 있더라도 미래에 변경해야 할 이유가 거의 없습니다. 복제가 저 품질과 테스트 부족보다 더 많은 자극을 주었을 때 저의 우선 순위는 벗어났습니다. 후자의 것이 최우선 순위가되어야합니다.

미니멀리즘을위한 코드 복제?

이것은 내 머리 속에 떠오른 재미있는 생각이지만, 거의 같은 기능을 수행하는 C 및 C ++ 라이브러리가 발생할 수있는 경우를 고려하십시오. 가장 중요한 것은 둘 다 유능하게 구현되고 테스트를 거쳤으며 신뢰할 수있는 것입니다. 불행히도 나는 완벽한 병렬 비교에 가까운 것을 찾지 못했기 때문에 여기에서 가설 적으로 말해야합니다. 그러나이 병렬 비교에서 찾은 가장 가까운 것은 C 라이브러리가 C ++에 비해 훨씬 작습니다 (때로는 코드 크기의 1/10).

그리고 그 이유는 하나의 정확한 유스 케이스 대신 가장 광범위한 유스 케이스를 처리하는 일반적인 방법으로 문제를 해결하기 위해서는 수백에서 수천 라인의 코드가 필요할 수 있지만 후자는 십여. 중복성에도 불구하고 표준 데이터 구조를 제공 할 때 C 표준 라이브러리가 열악하다는 사실에도 불구하고 동일한 문제를 해결하기 위해 인간의 손에 적은 코드를 생성하는 경우가 종종 있습니다. 이 두 언어 사이의 인간 경향의 차이. 하나는 매우 구체적인 유스 케이스에 대한 해결을 장려하고, 다른 하나는 가장 광범위한 유스 케이스에 대해 더 추상적이고 일반적인 솔루션을 홍보하는 경향이 있지만, 그 결과는

나는 다른 날 github에서 누군가의 raytracer를보고 있었고 C ++로 구현되어 장난감 raytracer에 대한 많은 코드가 필요했습니다. 코드를 보는 데 많은 시간을 소비하지는 않았지만 광선 추적 프로그램이 필요로하는 것보다 훨씬 많은 방식으로 처리되는 범용 구조의 보트로드가있었습니다. 필자는 C ++을 일종의 슈퍼 상향식 방식으로 사용했기 때문에 매우 일반적인 용도의 데이터 구조 라이브러리를 만드는 데 중점을 두었습니다. 문제를 해결 한 다음 실제 문제를 해결합니다. 그러나 이러한 일반적인 구조는 여기 저기에서 일부 중복성을 제거하고 새로운 상황에서 많은 재사용을 누릴 수 있지만, 그 대신에 불필요한 코드 / 기능의 보트로드와 약간의 중복성을 교환하여 프로젝트를 엄청나게 팽창시킬 수 있으며, 후자는 이전보다 유지 관리가 더 쉬운 것은 아닙니다. 반대로, 나는 가장 광범위한 요구에 맞서 설계 결정을 조율해야하는 일반적인 설계를 유지하기가 어렵 기 때문에 종종 유지 보수가 더 어렵다는 것을 알게된다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.