친구 클래스를 사용하여 C ++에서 개인 멤버 함수를 캡슐화-우수 사례 또는 남용?


12

따라서 다음과 같이하여 개인 함수를 헤더에 넣지 않는 것이 가능하다는 것을 알았습니다.

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        friend class PredicateList_HelperFunctions;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList_HelperFunctions
    {
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList_HelperFunctions::fullMatch(*this);
    }

private 함수는 헤더에 선언되지 않으며 헤더를 가져 오는 클래스의 소비자는 해당 헤더가 존재하는지 알 필요가 없습니다. 도우미 함수가 템플릿 인 경우 필요합니다 (대체 헤더에 전체 코드를 넣는 방법). 이것이 "발견 된"방법입니다. 개인 멤버 함수를 추가 / 제거 / 수정하는 경우 헤더를 포함하는 모든 파일을 다시 컴파일 할 필요가없는 또 다른 장점이 있습니다. 모든 개인 기능은 .cpp 파일에 있습니다.

그래서...

  1. 이것은 유명한 디자인 패턴입니까?
  2. 나에게 (자바 / C # 배경에서오고 내 시간에 C ++을 배우는), 헤더는 인터페이스를 정의하고 .cpp는 구현을 정의하기 때문에 (그리고 개선 된 컴파일 시간은 매우 좋은 것 같습니다. 좋은 보너스). 그러나 그것은 또한 그런 식으로 사용되지 않는 언어 기능을 남용하는 것처럼 냄새가납니다. 그래서, 그것은 무엇입니까? 이것이 전문적인 C ++ 프로젝트에서 볼 때 싫은 일입니까?
  3. 내가 생각하지 않는 함정은?

라이브러리 가장자리에서 구현을 숨기는 훨씬 강력한 방법 인 Pimpl에 대해 알고 있습니다. Pimpl이 성능 문제를 일으키거나 클래스를 값으로 취급해야하기 때문에 작동하지 않는 내부 클래스와 함께 사용하기에 적합합니다.


편집 2 : Dragon Energy의 탁월한 답변은 friend키워드를 전혀 사용하지 않는 다음 솔루션을 제안했습니다 .

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        class Private;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList::Private
    {
    public:
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList::Private::fullMatch(*this);
    }

이렇게하면 동일한 분리 원리를 유지하면서 충격 요인 friend(처럼 악마 화 된 것 goto)을 피할 수 있습니다.


2
" 소비자는 자체 클래스 PredicateList_HelperFunctions를 정의하고 개인 필드에 액세스 할 수 있습니다. " ODR 위반이 아닙니까? 귀하와 소비자 모두 동일한 클래스를 정의해야합니다. 이러한 정의가 동일하지 않으면 코드의 형식이 잘못됩니다.
Nicol Bolas

답변:


13

내가 이미 알고있는 것처럼 내가 처음으로 머리를 긁을 수있는 최소한의 말은 조금 어리석은 일입니다. 코드를 처음 발견했을 때 나는 당신이하고있는 일과 스타일을 집어 들기 시작할 때 까지이 도우미 클래스가 구현되는 곳을 궁금해합니다. / habits (이 시점에서 나는 완전히 익숙해 질 수 있음).

헤더의 정보량을 줄이는 것이 좋습니다. 특히 매우 큰 코드베이스에서는 컴파일 타임 종속성을 줄이고 궁극적으로 빌드 시간을 줄이는 실질적인 효과가 있습니다.

내 직감은 이런 식으로 구현 세부 사항을 숨길 필요가 있다고 생각하면 소스 파일의 내부 연결로 독립 기능에 매개 변수 전달을 선호한다는 것입니다. 일반적으로 클래스의 모든 내부에 액세스 할 필요없이 특정 클래스를 구현하는 데 유용한 유틸리티 함수 (또는 전체 클래스)를 구현할 수 있으며 대신 메소드 구현에서 함수 (또는 생성자)로 관련 클래스를 전달할 수 있습니다. 그리고 당연히 그것은 당신의 클래스와 "헬퍼"사이의 커플 링을 줄이는 보너스를 가지고 있습니다. 또한 둘 이상의 클래스 구현에 적용 할 수있는보다 일반화 된 목적을 제공하기 시작하면 "도우미"가 될 수있는 것을 일반화하는 경향이 있습니다.

또한 코드에서 많은 "도우미"를 볼 때 가끔 울었습니다. 항상 사실은 아니지만 때로는 함수를 분해하는 개발자의 증상으로 인해 엄청난 양의 데이터가 코드 중복을 제거하여 코드의 양을 줄이는 사실을 넘어 거의 이해할 수없는 이름 / 목적을 가진 함수로 코드 중복을 제거 할 수 있습니다 다른 기능을 구현하는 데 필요한 코드. 조금 더 작은 생각만으로도 클래스 구현이 추가 기능으로 분해되는 방식 측면에서 훨씬 더 명확해질 수 있으며 내부에 대한 전체 액세스 권한으로 객체의 전체 인스턴스를 전달하는 데 특정 매개 변수를 전달하는 것이 도움이 될 수 있습니다 그런 스타일의 디자인 사고를 장려하십시오. 물론 그렇게하고 있다고 제안하는 것은 아닙니다 (모릅니다).

그것이 다루기 어려워지면, 나는 두 번째, 더 관용적 인 해결책을 고려할 것입니다 (나는 당신이 그것에 대해 문제를 언급했지만 최소한의 노력으로 문제를 피하기 위해 해결책을 일반화 할 수 있다고 생각합니다). 그것은 개인 데이터를 포함하여 클래스 도매에 필요한 많은 정보를 헤더 도매에서 옮길 수 있습니다. 완전한 사용자 정의 카피 ctor를 구현하지 않고도 가치 의미를 보존하면서 프리리스트와 같은 더티 쉐프 일정 시간 할당 기 *를 통해 pimpl의 성능 문제를 크게 완화 할 수 있습니다.

  • 성능 측면에서 pimpl은 최소한 포인터 오버 헤드를 발생 시키지만 실제 상황에서 포즈가 매우 심각한 경우가 있다고 생각합니다. 할당자를 통해 공간 위치가 크게 저하되지 않으면 객체를 반복하는 긴밀한 루프 (성능이 그다지 중요하지 않은 경우 일반적으로 균일해야 함)는 실제로 다음과 같은 것을 사용하는 경우 캐시 누락을 최소화하는 경향이 있습니다. 클래스의 필드를 크게 연속 된 메모리 블록에 배치하여 pimpl을 할당 할 수있는 비어있는 목록.

개인적으로 그러한 가능성을 소진 한 후에야 이와 같은 것을 고려할 것입니다. 대안이 헤더에 노출 된 더 개인적인 방법과 실질적인 관심사가 될 수있는 난해한 성격이라고 생각하면 괜찮은 생각이라고 생각합니다.

대안

친구가없는 동일한 목적을 달성하기 위해 지금 내 머릿속에 들어온 한 가지 대안은 다음과 같습니다.

struct PredicateListData
{
     int somePrivateField;
};

class PredicateList
{
    PredicateListData data;
public:
    bool match() const;
};

// In source file:
static bool fullMatch(const PredicateListData& p)
{
     // Can access p.somePrivateField here.
}

bool PredicateList::match() const
{
     return fullMatch(data);
}

이제는 매우 차이가있는 것처럼 보일 수 있으며 여전히 "도우미"라고 부를 것입니다 (모두 필요 여부에 관계없이 클래스의 전체 내부 상태를 함수에 전달하기 때문에 아마도 무시할만한 의미로) "충격"요소가 발생하지 않는 것을 제외하고 friend. 일반적으로 friend클래스 내부는 다른 곳에서 액세스 할 수 있다고 말합니다 (자체 불변성을 유지할 수 없다는 의미가 있음). 당신이 사용하는 방식으로 friend사람들이 연습을 알고 있다면 오히려 무례하게됩니다.friend클래스의 개인 기능을 구현하는 데 도움이되는 동일한 소스 파일에 상주하고 있지만 위의 내용은 적어도 모든 종류의 친구를 포함하지 않는 친구를 포함하지 않을 수있는 논쟁의 여지가있는 이점으로 거의 동일한 효과를 달성합니다. 이 수업에 친구가 있습니다 위의 버전은 즉시의 구현에서 수행 된 작업 이외의 방법으로 개인에 액세스하거나 변경할 수있는 방법이 없음을 즉시 전달합니다 PredicateList.

누군가가 균일하게 이름을 지정 *Helper*하고 클래스의 개인 구현의 일부로 함께 묶여있는 동일한 소스 파일에 모두 넣을 수 있다면 누구나이 수준의 뉘앙스로 다소 독단적 인 영토로 이동하고 있습니다 . 그러나 우리가 nit-picky를 얻는다면 아마도 위의 스타일이 friend약간 무서워 보이는 키워드가 없으면 무릎을 꿇는 반응을 한 눈에 많이 일으키지 않을 것입니다 .

다른 질문들 :

소비자는 자체 클래스 PredicateList_HelperFunctions를 정의하고 개인 필드에 액세스 할 수 있습니다. 나는 이것을 큰 문제로 보지 않지만 (사실 현장에서 실제로 캐스팅을 할 수 있다면) 소비자가 그런 식으로 사용하도록 권장 할 수 있습니까?

클라이언트가 동일한 이름으로 두 번째 클래스를 정의하고 연결 오류없이 내부에 액세스 할 수있는 API 경계에서 가능성이 있습니다. 다시 한 번 저는이 수준의 "만약"수준의 안전 문제가 우선 순위 목록에서 매우 낮은 그래픽 작업을하는 C 코더입니다. 존재하지 않는 것처럼 가장하려고 노력하십시오. :-D 만약 당신이 이와 같은 우려가 다소 심각한 영역에서 일하고 있다면, 나는 그것을 고려해야 할 적절한 생각이라고 생각합니다.

위의 대안 제안은 또한이 문제를 겪지 않도록합니다. 그래도 계속 사용하고 싶다면 friend도우미를 개인 중첩 클래스로 만들어 문제를 피할 수도 있습니다.

class PredicateList
{
    ...

    // Declare nested class.
    class Helper;

    // Make it a friend.
    friend class Helper;

public:
    ...
};

// In source file:
class PredicateList::Helper
{
    ...
};

이것은 유명한 디자인 패턴입니까?

내 지식에는 없습니다. 실제로 구현 세부 사항과 스타일을 최소화하기 때문에 하나가있을 것입니다.

"도움말 지옥"

"도우미"코드가 많은 구현을 볼 때 때때로 울부 짖는 방법에 대한 추가 설명이 필요합니다. "도우미"를 찾기 위해 클래스를 구현 한 동료들의 :-D 그리고 나는이 헬퍼들이 정확히 무엇을해야하는지 알아 내려고 노력하는 팀의 유일한 팀이 아니 었습니다. 또한 "도우미를 쓰지 말아야 한다 " 와 같은 독단적 인 태도를 취하고 싶지는 않지만 , 실용적이지 않은 것들을 구현하는 방법에 대해 생각하는 것이 도움이 될 것이라는 작은 제안을 할 것입니다.

모든 개인 멤버 함수가 정의에 따라 도우미 함수가 아닙니까?

그리고 네, 나는 개인 방법을 포함하고 있습니다. 나는 간단한 공용 인터페이스 등을하지만, 다소처럼 목적에 잘못 정의되어 개인 방법의 끝없는 세트와 같은 클래스를 참조하는 경우 find_implfind_detail또는 find_helper, 나는 또한 비슷한 방법으로 싫증이 나다.

대안으로 제안하는 static것은 "다른 사람을 구현하는 데 도움이되는 함수"보다 적어도 더 일반적인 목적으로 클래스를 구현하는 데 도움이 되는 내부 연결 ( 익명 네임 스페이스 또는 선언 된 네임 스페이스) 이있는 비회원 비 친구 함수입니다 . 그리고 나는 일반적인 SE 관점에서 이것이 바람직한 이유에 대해 C ++ '코딩 표준'의 Herb Sutter를 인용 할 수 있습니다.

멤버쉽 비용을 피하십시오 : 가능하면 비회원이 아닌 기능을 수행하는 것을 선호하십시오. [...] 비회원 비 친구 함수는 종속성을 최소화하여 캡슐화를 향상시킵니다. 함수의 본문은 클래스의 비공개 멤버에 의존 할 수 없습니다 (항목 11 참조). 또한 분리 가능한 기능을 해방하기 위해 모 놀리 식 클래스를 분리하여 커플 링을 추가로 줄입니다 (항목 33 참조).

변수 범위를 좁히는 기본 원칙의 관점에서 그가 말하는 "회원 요금"도 이해할 수 있습니다. 가장 극단적 인 예로, 전체 프로그램을 실행하는 데 필요한 모든 코드가있는 God 객체를 상상하면 모든 내부에 액세스 할 수있는 이런 종류의 "헬퍼"(멤버 기능 또는 친구 여부)를 선호합니다 ( 클래스의 privates)는 기본적으로 이러한 변수를 전역 변수보다 문제가되지 않습니다. 이 가장 극단적 인 예에서 상태를 올바르게 관리하고 스레드 안전성을 유지하고 전역 변수를 사용하여 변하지 않는 변수를 유지 관리하는 데 어려움이 있습니다. 물론 대부분의 실제 사례는이 극단에 가까운 곳은 아니지만 정보 숨기기는 액세스하는 정보의 범위를 제한하는 것만 큼 유용합니다.

이제 Sutter는 이미 여기에 좋은 설명을 제공하지만, 디커플링이 기능 설계 방식 측면에서 (적어도 뇌가 내 것처럼 작동한다면) 심리적 개선처럼 촉진되는 경향이 있음을 더 덧붙입니다. 전달하는 관련 매개 변수 만 제외하고 클래스의 모든 항목에 액세스 할 수없는 함수 설계를 시작할 때 또는 클래스의 인스턴스를 매개 변수로 공개 멤버 만 전달하면 선호하는 디자인 사고 방식을 홍보하는 경향이 있습니다. 디커플링과 개선 된 캡슐화 위에 명확한 목적을 가진 기능은 모든 것에 액세스 할 수 있다면 디자인하려는 유혹보다 더 나은 기능입니다.

우리가 사지로 돌아 가면 전역 변수로 가득 찬 코드베이스는 개발자가 목적에 명확하고 일반화 된 방식으로 함수를 디자인하도록 유혹하지 않습니다. 함수에서 액세스 할 수있는 정보가 많을수록 필사자 중 많은 사람들이 함수를 축약 화하려는 유혹에 직면하여 해당 함수에 대해보다 구체적이고 관련성있는 매개 변수를 받아들이는 대신이 모든 추가 정보에 액세스하기 위해 명확성을 줄입니다. 국가에 대한 접근을 좁히고 적용 성을 넓히고 의도의 명확성을 향상시킨다. 그것은 회원 기능이나 친구에게 적용됩니다 (일반적으로 어느 정도는 아니지만).


1
입력 주셔서 감사합니다! 그래도이 부분에서 당신이 어디에서 왔는지는 완전히 이해하지 못합니다. "코드에서"도우미 "가 많을 때 가끔 울기도합니다." -모든 비공개 멤버 함수가 정의에 따라 도우미 함수가 아닙니까? 이것은 일반적으로 개인 멤버 기능에 문제가있는 것 같습니다.
Robert Fraser

1
아, 내부 클래스는 전혀 "친구"가 필요하지 않으므로 그렇게하면 "친구"키워드를 완전히 피할 수 있습니다
Robert Fraser

"모든 개인 멤버 함수가 정의에 따라 헬퍼 함수가 아닌가? 이것은 일반적으로 개인 멤버 함수에 문제가있는 것으로 보인다." 가장 큰 것은 아닙니다. 나는 사소한 클래스 구현의 경우, 많은 개인 기능이나 모든 클래스 멤버에게 한 번에 액세스 할 수있는 도우미가 필요하다는 실질적인 필요성을 생각했습니다. 그러나 나는 Linus Torvalds, John Carmack과 같은 일부 위대한 스타일을 보았고 C의 이전 코드는 객체와 비슷한 유사체를 코딩 할 때 일반적으로 Carmack과 함께 코딩하지 않습니다.
드래곤 에너지

그리고 당연히 소스 파일의 도우미는 클래스를 구현하는 데 많은 개인 함수를 사용했기 때문에 필요한 것보다 많은 외부 헤더를 포함하는 대규모 헤더보다 선호됩니다. 그러나 위의 스타일과 다른 스타일을 연구 한 후 클래스의 모든 내부 멤버에 액세스하여 하나의 클래스를 구현하기 위해 필요한 클래스보다 약간 더 일반화 된 함수를 작성하는 것이 가능하다는 것을 깨달았습니다. 함수의 이름을 잘 정하고 그것을 작동시키는 데 필요한 특정 멤버를 전달하면 더 많은 시간을 절약하게됩니다 [...]
Dragon Energy

[...]보다 걸리므로 나중에 조작하기가 더 쉬운 전반적인 구현이 가능합니다. 그것은 "전체 일치"에 대한 "도우미 술어"를 작성하는 것과 같지 않습니다 PredicateList. 종종 술어 목록의 멤버 또는 두 개를 액세스 할 필요가없는 약간 더 일반화 된 함수로 전달하는 것이 가능할 수도 있습니다. 의 모든 개인 구성원은 PredicateList종종 "후시 코드 재사용"에 대한 더 많은 기회뿐만 아니라 해당 내부 기능에 대해보다 명확하고 일반적인 이름과 목적을 제공하는 경향이 있습니다.
드래곤 에너지
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.