임시 범위에 대한 참조로 인해이 범위에 대한 책임은 누구에게 있습니까?


15

다음 코드는 처음에는 무해한 것으로 보입니다. 사용자는이 기능 bar()을 사용하여 일부 라이브러리 기능과 상호 작용합니다. (이것은 bar()비 일시적 값 또는 이와 유사한 것에 대한 참조를 반환 한 이후 로 오랫동안 작동했을 수도 있습니다 .) 그러나 이제는 단순히의 새로운 인스턴스를 반환합니다 B. B다시 반복 a()가능한 유형의 객체에 대한 참조를 반환 하는 함수 가 A있습니다. 사용자는이 객체를 쿼리하려고합니다.이 객체 는 반복이 시작되기 전에 B반환 된 임시 객체 bar()가 소멸 되므로 segfault로 이어집니다 .

나는 누구 (도서관 또는 사용자)가 이것을 비난 해야하는지 결정적입니다. 모든 라이브러리 제공 클래스는 깨끗해 보였으며 확실히 다른 코드와는 다른 (멤버에 대한 참조 반환, 스택 인스턴스 반환 등) 아무것도하지 않습니다. 사용자는 아무 것도 잘못하지 않는 것 같습니다. 그는 객체의 수명과 관련하여 아무것도하지 않고 일부 객체를 반복하고 있습니다.

(관련 질문은 다음과 같습니다. 코드가 루프 헤더에서 둘 이상의 체인 호출에 의해 검색된 항목에 대해 코드가 "범위에 따라 범위를 정하지 않아야"한다는 일반적인 규칙을 설정해야합니다. rvalue?)

#include <algorithm>
#include <iostream>

// "Library code"
struct A
{
    A():
        v{0,1,2}
    {
        std::cout << "A()" << std::endl;
    }

    ~A()
    {
        std::cout << "~A()" << std::endl;
    }

    int * begin()
    {
        return &v[0];
    }

    int * end()
    {
        return &v[3];
    }

    int v[3];
};

struct B
{
    A m_a;

    A & a()
    {
        return m_a;
    }
};

B bar()
{
    return B();
}

// User code
int main()
{
    for( auto i : bar().a() )
    {
        std::cout << i << std::endl;
    }
}

6
누가 책임을 져야하는지 알아낼 때 다음 단계는 무엇입니까? 그 / 그녀를 고함?
JensG

7
아뇨, 왜요? 나는이 "프로그램"을 개발하는 사고 과정이 미래에이 문제를 피하지 못한 곳을 아는 것이 실제로 더 흥미 롭다.
hllnll

이것은 rvalue 또는 범위 기반 for 루프와 관련이 없지만 사용자가 객체 수명을 올바르게 이해하지 못합니다.
제임스

현장 비고 : 이것은 결함이 아닌 것으로 폐쇄 된 CWG 900 입니다. 아마도 회의록에 토론이 포함되어있을 수도 있습니다.
dyp

8
누가 이것을 비난해야합니까? Bjarne Stroustrup과 Dennis Ritchie, 무엇보다도.
메이슨 휠러

답변:


14

근본적인 문제는 C ++의 언어 기능 (또는 그 부족)의 조합이라고 생각합니다. 라이브러리 코드와 클라이언트 코드는 모두 합리적입니다 (문제가 명백하지 않다는 사실에 의해 입증 됨). 임시 수명이 B루프 끝까지 연장 된 경우 문제가 없습니다.

임시 생활을 충분히 길고 더 이상 만들지 않는 것은 매우 어렵습니다. "루프가 끝날 때까지 라이브 기반 범위의 범위를 만드는 데 관련된 모든 임시"조차 부작용이없는 것은 아닙니다. 값에 의해 객체와 B::a()독립적 인 범위를 반환하는 경우를 고려하십시오 B. 그런 다음 임시 B를 즉시 버릴 수 있습니다. 수명 연장이 필요한 경우를 정확하게 식별 할 수 있더라도 프로그래머에게는 분명하지 않기 때문에 그 효과 (놀랍게도 소멸자)는 놀라 울 정도로 미묘한 버그의 원인 일 수 있습니다.

이러한 넌센스를 감지하고 금지하여 프로그래머가 bar()지역 변수 로 명시 적으로 상승시키는 것이 더 바람직 합니다. 이것은 C ++ 11에서는 불가능하며 주석이 필요하기 때문에 불가능할 것입니다. Rust는 다음과 같은 서명을 .a()합니다 :

fn a<'x>(bar: &'x B) -> &'x A { bar.a }
// If we make it as explicit as possible, or
fn a(&self) -> &A { self.a }
// if we make it a method and rely on lifetime elision.

여기 'x에 수명 변수 또는 지역이 있습니다. 이는 자원을 사용할 수있는 기간의 상징적 이름입니다. 솔직히 말해서, 생애를 설명하기가 어렵거나 아직 가장 잘 설명하지 못 했으므로이 예제에 필요한 최소한으로 제한하고 기울어 진 독자에게 공식 문서 를 참조 할 것 입니다.

빌리 체커는 bar().a()루프가 실행 되는 한 결과 가 살아남 아야 함을 알 수 있습니다 . 수명에 제약 조건으로 표현하면 'x, 우리는 쓰기 : 'loop <= 'x. 또한 메소드 호출의 수신자 bar()가 임시 인 것을 알 수 있습니다. 두 포인터는 동일한 수명과 연관되므로 'x <= 'temp또 다른 제약이 있습니다.

이 두 제약은 모순입니다! 우리는 필요 'loop <= 'x <= 'temp하지만, 'temp <= 'loop꽤 정확하게 문제를 포착한다. 상충되는 요구 사항으로 인해 버그가있는 코드는 거부됩니다. 이것은 컴파일 타임 검사이며 Rust 코드는 일반적으로 동등한 C ++ 코드와 동일한 머신 코드가되므로 런타임 비용을 지불 할 필요가 없습니다.

그럼에도 불구하고 이것은 언어에 추가하기위한 큰 기능이며 모든 코드에서 사용하는 경우에만 작동합니다. API 디자인도 영향을받습니다 (C ++에서 너무 위험한 일부 디자인은 실용적이게되고 다른 디자인은 평생 동안 멋지게 플레이 할 수 없습니다). 아아, 그것은 C ++ (또는 실제로 어떤 언어)에 소급하여 추가하는 것이 실용적이지 않다는 것을 의미합니다. 요약하자면, 결점은 성공적인 언어가 가지고있는 관성과 1983 년 Bjarne이 지난 30 년간의 연구와 C ++ 경험의 교훈을 통합 할 수있는 수정 구슬과 예측력이 없다는 사실에 있습니다. ;-)

물론, 앞으로 문제를 피하는 데 전혀 도움이되지 않습니다 (Russt로 전환하고 C ++을 다시 사용하지 않는 한). 여러 개의 체인 메소드 호출로 더 긴 표현식을 피할 수 있습니다 (이는 매우 제한적이며 모든 평생 문제를 원격으로 해결하지도 않습니다). 또는 하나의 컴파일러 지원없이 더 훈련 소유권 정책을 채택 시도 할 수 : 문서는 명확하게 bar결과 값을 기준으로 그 반환 B::a()해야는 오래 살 수 없습니다 B하는 a()호출됩니다. 수명이 긴 참조 대신 값으로 반환되도록 함수를 변경할 때는 이것이 계약 변경 임을 인식하십시오 . 여전히 오류가 발생하기는하지만 원인이 발생할 때 원인을 식별하는 프로세스 속도가 빨라질 수 있습니다.


14

C ++ 기능을 사용하여이 문제를 해결할 수 있습니까?

C ++ 11에는 멤버 함수 참조 한정자가 추가되어 멤버 함수가 호출 될 수있는 클래스 인스턴스 (표현)의 값 범주를 제한 할 수 있습니다. 예를 들면 다음과 같습니다.

struct foo {
    void bar() & {} // lvalue-ref-qualified
};

foo& lvalue ();
foo  prvalue();

lvalue ().bar(); // OK
prvalue().bar(); // error

호출 할 때 begin멤버 함수를, 우리는 가장 가능성이 우리는 또한 전화를해야한다는 점을 알고 end(또는 같은 멤버 함수를 size범위의 크기를 얻기 위해). 이를 위해서는 lvalue를 두 번 처리해야하므로 lvalue로 작업해야합니다. 따라서 이러한 멤버 함수는 lvalue-ref-qualified 여야한다고 주장 할 수 있습니다.

그러나 이것은 기본 문제인 앨리어싱을 해결하지 못할 수 있습니다. beginend멤버 함수 별칭 객체 또는 객체가 관리하는 자원. 단일 함수로 대체 begin하고 rvalues에서 호출 할 수있는 end기능 range을 제공해야합니다.

struct foo {
    vector<int> arr;

    auto range() & // C++14 return type deduction for brevity
    { return std::make_pair(arr.begin(), arr.end()); }
};

for(auto const& e : foo().range()) // error

이것은 유효한 유스 케이스 일 수 있지만 위의 정의는 range허용하지 않습니다. 멤버 함수 호출 후 임시 주소를 지정할 수 없으므로 컨테이너, 즉 소유 범위를 반환하는 것이 더 합리적 일 수 있습니다.

struct foo {
    vector<int> arr;

    auto range() &
    { return std::make_pair(arr.begin(), arr.end()); }

    auto range() &&
    { return std::move(arr); }
};

for(auto const& e : foo().range()) // OK

이것을 OP의 경우에 적용하고 약간의 코드 검토

struct B {
    A m_a;
    A & a() { return m_a; }
};

이 멤버 함수는 표현식의 값 범주를 변경합니다 B(). prvalue이지만 B().a()lvalue입니다. 반면에 B().m_arvalue입니다. 먼저 이것을 일관성있게 만들어 봅시다. 이를 수행하는 두 가지 방법이 있습니다.

struct B {
    A m_a;
    A &  a() &  { return m_a; }

    A && a() && { return std::move(m_a); }
    // or
    A    a() && { return std::move(m_a); }
};

위에서 말한 것처럼 두 번째 버전은 OP의 문제를 해결합니다.

또한 B의 멤버 함수를 제한 할 수 있습니다 .

struct A {
    // [...]

    int * begin() & { return &v[0]; }
    int * end  () & { return &v[3]; }

    int v[3];
};

:범위 기반 for 루프에서 뒤에 나오는 표현식의 결과가 참조 변수에 바인딩 되므로 OP 코드에 영향을 미치지 않습니다 . 그리고이 변수 ( begin및 그 end멤버 함수 에 액세스하는 데 사용되는 표현식 )는 lvalue입니다.

물론, 기본 규칙이 "rvalue에 대한 앨리어싱 멤버 함수를 앨리어싱 해야 할 이유가없는 한 모든 리소스를 소유 한 객체를 반환해야하는지" 여부가 문제입니다 . 그것이 반환하는 별칭은 합법적으로 사용될 수 있지만, 겪고있는 방식으로 위험합니다. "부모"임시의 수명을 연장하는 데 사용할 수 없습니다 :

// using the OP's definition of `struct B`,
// or version 1, `A && a() &&;`

A&&      a = B().a(); // bug: binds directly, dangling reference
A const& a = B().a(); // bug: same as above
A        a = B().a(); // OK

A&&      a = B().m_a; // OK: extends the lifetime of the temporary

C ++ 2a에서는 다음과 같이이 문제를 해결해야한다고 생각합니다.

for( B b = bar(); auto i : b.a() )

OP 대신

for( auto i : bar().a() )

해결 방법은 수명이 bfor 루프의 전체 블록 임을 수동으로 지정합니다 .

이 init-statement를 도입 한 제안

라이브 데모


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