IT TIP

값에 의한 전달 및 이동이 잘못된 관용구를 구성합니까?

itqueen 2020. 12. 8. 20:36
반응형

값에 의한 전달 및 이동이 잘못된 관용구를 구성합니까?


우리는 C ++에서 이동 의미 체계를 가지고 있기 때문에 요즘에는 일반적으로

void set_a(A a) { _a = std::move(a); }

이유 a는가 rvalue이면 복사본이 제거되고 한 번만 이동하기 때문입니다.

하지만 alvalue 이면 어떻게 될까요? 복사 구성과 이동 할당이있는 것 같습니다 (A에 적절한 이동 할당 연산자가 있다고 가정). 개체에 멤버 변수가 너무 많으면 이동 할당에 많은 비용이들 수 있습니다.

반면에 우리가

void set_a(const A& a) { _a = a; }

복사 할당은 하나만 있습니다. lvalue를 전달하는 경우 값별 전달 관용구보다이 방법이 선호된다고 말할 수 있습니까?


이동 비용이 많이 드는 유형은 최신 C ++ 사용에서 드뭅니다. 이동 비용이 염려되는 경우 두 오버로드를 모두 작성하십시오.

void set_a(const A& a) { _a = a; }
void set_a(A&& a) { _a = std::move(a); }

또는 완벽한 전달 세터 :

template <typename T>
void set_a(T&& a) { _a = std::forward<T>(a); }

lvalues, rvalues ​​및 decltype(_a)추가 복사 나 이동없이 암시 적으로 변환 할 수있는 모든 항목을 허용 합니다.

lvalue에서 설정할 때 추가 이동이 필요함에도 불구하고 (a) 대부분의 유형이 일정한 시간 이동을 제공하고 (b) 복사 및 교체가 단일에서 예외 안전성과 거의 최적의 성능을 제공하기 때문에 관용구는 나쁘지 않습니다. 코드 줄.


하지만 alvalue 이면 어떻게 될까요? 복사 구성과 이동 할당이있는 것 같습니다 (A에 적절한 이동 할당 연산자가 있다고 가정). 개체에 멤버 변수가 너무 많으면 이동 할당에 많은 비용이들 수 있습니다.

문제가 잘 발견되었습니다. 나는 가치에 의한 통과 후 이동 구조가 나쁜 관용구라고 말할 수는 없지만 잠재적 인 함정이 분명히 있습니다.

유형을 이동하는 데 비용이 많이 들고 / 또는 이동하는 것이 본질적으로 복사 일 경우 값별 전달 방식은 차선책입니다. 이러한 유형의 예로는 고정 크기 배열을 멤버로 사용하는 유형이 있습니다. 이동하는 데 상대적으로 비용이 많이 들고 이동은 복사본 일뿐입니다. 또한보십시오

이 맥락에서.

가치 별 전달 방식은 하나의 기능 만 유지하면되지만 성능에 따라 비용을 지불해야한다는 이점이 있습니다. 이 유지 관리 이점이 성능 손실보다 중요한지 여부는 응용 프로그램에 따라 다릅니다.

lvalue 및 rvalue 참조 방식에 의한 전달은 여러 인수가있는 경우 유지 관리 문제로 빠르게 이어질 수 있습니다. 이걸 고려하세요:

#include <vector>
using namespace std;

struct A { vector<int> v; };
struct B { vector<int> v; };

struct C {
  A a;
  B b;
  C(const A&  a, const B&  b) : a(a), b(b) { }
  C(const A&  a,       B&& b) : a(a), b(move(b)) { }
  C(      A&& a, const B&  b) : a(move(a)), b(b) { }
  C(      A&& a,       B&& b) : a(move(a)), b(move(b)) { }  
};

인수가 여러 개인 경우 순열 문제가 발생합니다. 이 매우 간단한 예에서이 4 개의 생성자를 유지하는 것은 여전히 ​​나쁘지 않습니다. 그러나 이미이 간단한 경우에서는 단일 함수로 값에 의한 전달 방식을 사용하는 것을 진지하게 고려할 것입니다.

C(A a, B b) : a(move(a)), b(move(b)) { }

위의 4 개의 생성자 대신.

짧게 말하면 두 접근법 모두 단점이 없습니다. 조기에 최적화하는 대신 실제 프로파일 링 정보를 기반으로 결정을 내립니다.


값이 저장되는 일반적인 경우 에는 값 에 의한 전달 만이 좋은 절충안입니다.

lvalue 만 전달된다는 것을 알고있는 경우 (일부 밀접하게 결합 된 코드) 이는 비합리적이고 현명하지 않습니다.

둘 다 제공하여 속도 향상이 의심되는 경우 먼저 THINK TWICE, 도움이되지 않으면 MEASURE.

값이 저장되지 않는 곳에서는 불필요한 복사 작업을 많이 방지하기 때문에 참조에 의한 전달을 선호합니다.

마지막으로, 프로그래밍이 무심코 규칙을 적용하는 것으로 축소 될 수 있다면 로봇에게 맡길 수 있습니다. 그래서 IMHO는 규칙에 너무 집중하는 것은 좋은 생각이 아닙니다. 상황에 따라 장점과 비용에 초점을 맞추는 것이 좋습니다. 비용에는 속도뿐만 아니라 코드 크기와 명확성이 포함됩니다. 규칙은 일반적으로 그러한 이해 상충을 처리 할 수 ​​없습니다.


현재 답변은 매우 불완전합니다. 대신 내가 찾은 장단점 목록을 기반으로 결론을 내리려고 노력할 것입니다.

짧은 답변

요컨대 괜찮을 수도 있지만 때로는 나쁘기도합니다.

이 관용구, 즉 통합 인터페이스는 포워딩 템플릿 또는 다른 오버로드에 비해 더 명확합니다 (개념 설계 및 구현 모두에서). 때때로 복사 및 교체와 함께 사용됩니다 (실제로 는이 경우 이동 및 교체도 마찬가지 임).

상세한 분석

장점은 다음과 같습니다.

  • 각 매개 변수 목록에 대해 하나의 기능 만 필요합니다.
    • 그것은 참으로 하나가 아닌 여러 일반 과부하 (또는 필요가 2 n 개의 당신이 과부하 n은 각각이 규정 또는이 될 수있는 매개 변수 constrestrict로).
    • 전달 템플릿 내에서와 마찬가지로 값으로 전달되는 매개 변수는 const,뿐만 아니라 volatile, 더 많은 일반적인 과부하를 줄여줍니다.
      • 위의 총알과 결합하여, 당신은 필요하지 않습니다 4 N {unqulified에 봉사하는 오버로드 const, const, const volatile}의 조합 N 매개 변수를 설정합니다.
    • 전달 템플릿과 비교할 때 매개 변수가 일반 (템플릿 유형 매개 변수를 통해 매개 변수화 됨) 일 필요가없는 한 템플릿이 아닌 함수일 수 있습니다. 이를 통해 각 번역 단위의 각 인스턴스에 대해 인스턴스화해야하는 템플릿 정의 대신 라인 외부 정의를 사용할 수 있으므로 번역 시간 성능을 크게 향상시킬 수 있습니다 (일반적으로 컴파일 및 링크 중).
    • 또한 다른 오버로드 (있는 경우)를 더 쉽게 구현할 수 있습니다.
      • 당신이 파라미터 객체 유형에 대한 전달 템플릿이있는 경우 T, 그것은 여전히 과부하가 매개 변수를 갖는 충돌 할 수 있습니다 const T&인수가 유형의 좌변이 될 수 있기 때문에, 같은 위치를 T입력 인스턴스화 템플릿 T&(보다는 const T&더 할 수 있습니다에 대한) 가장 좋은 과부하 후보를 구별 할 다른 방법이없는 경우 과부하 규칙에 의해 선호됩니다. 이러한 불일치는 상당히 놀라운 것일 수 있습니다.
      • 특히 P&&클래스에 하나의 매개 변수 유형이있는 전달 템플릿 생성자가 있다고 가정합니다 C. 복사 / 이동 생성자와 충돌하지 않도록 (예 : template-parameter-list 에 추가 하여) SFINAE에 의해 P&&cv-qualified 가능성이 있는 인스턴스를 제외하는 것을 몇 번 잊으 C시겠습니까? 사용자 제공)?typename = enable_if_t<!is_same<C, decay_t<P>>
  • 매개 변수가 비 참조 유형의 값으로 전달되기 때문에 인수가 prvalue 로 전달되도록 강제 할 수 있습니다 . 이것은 인수가 클래스 리터럴 유형일 때 차이를 만들 수 있습니다 . constexpr클래스 외부 정의없이 일부 클래스에 선언 된 정적 데이터 멤버가있는 클래스가 있다고 가정합니다. lvalue 참조 유형의 매개 변수에 대한 인수로 사용되면 ODR 이므로 결국 링크에 실패 할 수 있습니다. -사용 되었으며 정의가 없습니다.

단점은 다음과 같습니다.

  • 통합 인터페이스는 매개 변수 객체 유형이 클래스와 동일한 경우 복사 및 이동 생성자를 대체 할 수 없습니다. 그렇지 않으면 매개 변수의 복사 초기화는 통합 생성자를 호출하고 생성자가 자신을 호출하기 때문에 무한 재귀가됩니다.
  • 다른 답변에서 언급했듯이 복사 비용을 무시할 수없는 경우 (저렴하고 예측 가능) 이는 복사가 필요하지 않을 때 호출에서 거의 항상 성능 저하가 있음을 의미합니다 . 통합의 복사 초기화가 통과했기 때문입니다. 에 의하여 - 값 매개 변수는 무조건 소개 사본 하지 않는 한 인수 (하나에-복사하거나 이동-에)를 생략 .
    • 심지어와 필수 생략 구현 시도하지 않는 한 - C ++ (17)은 매개 변수 객체의 멀리 제거 여전히 거의 무료로 초기화 복사하기 때문에 매우 열심히 증명하기 위해 행동에 따라 변경되지 같은-경우 규칙 대신에의 전용 복사 생략 규칙 때로는 수 있습니다 여기에 적용 불가능 전체 프로그램 분석없이.
    • 마찬가지로, 특히 사소하지 않은 하위 객체가 고려 될 때 (예 : 컨테이너의 경우) 파괴 비용도 무시할 수 없습니다. 차이점은 복사 구성에 의해 도입 된 복사 초기화에만 적용되는 것이 아니라 이동 구성에도 적용된다는 것입니다. 생성자에서 복사보다 저렴하게 이동하면 상황을 개선 할 수 없습니다. 복사 초기화 비용이 많을수록 파기 비용도 커집니다.
  • 사소한 단점은 복수의 오버로드와 같이 다른 방식으로 인터페이스를 조정할 수있는 방법이 없다는 것입니다. 예를 들어 noexcept매개 변수 const&&&정규화 된 유형에 대해 다른 지정자를 지정합니다.
    • 이 예에서 OTOH는 일반적으로 를 지정하는 경우 noexcept(false)복사 + noexcept이동을 제공 noexcept하거나 noexcept(false)아무것도 지정하지 않을 때 (또는 명시 적 noexcept(false)) 항상 제공합니다 . (전자의 경우 noexcept함수 본문에서 벗어난 인수를 평가하는 동안에 만 발생하므로 복사 중 throw를 방지하지 않습니다.) 별도로 조정할 기회가 더 이상 없습니다.
    • 이것은 실제로 자주 필요하지 않기 때문에 사소한 것으로 간주됩니다.
    • 이러한 오버로드가 사용 되더라도 본질적으로 혼동 될 수 있습니다. 다른 지정자는 추론하기 어려운 미묘하지만 중요한 동작 차이를 숨길 수 있습니다. 과부하 대신 다른 이름이 아닌 이유는 무엇입니까?
    • -specification이 이제 함수 유형에 영향을 미치기noexcept 때문에 의 예는 C ++ 17 이후 특히 문제가 될 수 있습니다 . (일부 예기치 않은 호환성 문제는 Clang ++ 경고 로 진단 할 수 있습니다 .)noexcept

때때로 무조건 복사가 실제로 유용합니다. 강력한 예외 보장이있는 작업의 구성은 본질적으로 보증을 유지하지 않기 때문에 강력한 예외 보장이 필요하고 작업이 덜 엄격한 작업 순서로 분류 될 수없는 경우 사본을 초국적 국가 보유자로 사용할 수 있습니다. (예외 없음 또는 강력한) 예외 보장. ( 일반적으로 다른 이유로 할당 을 통합 하지 않는 것이 좋지만 여기에는 복사 및 스왑 관용구가 포함 됩니다. 아래를 참조하십시오.) 그러나 이것이 복사가 허용되지 않는다는 것을 의미하지는 않습니다. 인터페이스의 의도가 항상 유형의 일부 개체를 만드는 T것이며 이동 비용을 T무시할 수있는 경우 원하지 않는 오버 헤드없이 복사본을 대상으로 이동할 수 있습니다.

결론

따라서 일부 주어진 작업의 경우 통합 인터페이스를 사용하여 대체할지 여부에 대한 제안이 있습니다.

  1. 모든 매개 변수 유형이 통합 인터페이스와 일치하지 않거나 통합되는 작업간에 새 복사본 비용 이외의 동작 차이가있는 경우 통합 인터페이스가있을 수 없습니다.
  2. 다음 조건이 모든 매개 변수에 적합 하지 않으면 통합 인터페이스가있을 수 없습니다. (하지만 여전히 다른 명명 된 함수로 분리되어 하나의 호출을 다른 호출에 위임 할 수 있습니다.)
  3. 유형의 매개 변수에 T대해 모든 작업에 각 인수의 복사본이 필요한 경우 통합을 사용합니다.
  4. 복사 및 이동 구성 모두 T무시할 수있는 비용이있는 경우 통합을 사용하십시오.
  5. 인터페이스의 의도가 항상 유형의 일부 개체를 만드는 T것이며의 이동 구성 비용을 T무시할 수있는 경우 통합을 사용하십시오.
  6. 그렇지 않으면 통합을 피하십시오.

통합을 피하기 위해 필요한 몇 가지 예는 다음과 같습니다.

  1. 대한 (일반적으로 복사 및 스왑 관용구와 그 하위 객체에 할당 포함) 할당 작업 T통일의 기준을 충족하지 않는 복사 및 이동 구조물의 무시할 비용없이 할당의 의도는 할 수 없기 때문에, 생성 (그러나 대체 을 내용) 개체. 복사 된 객체는 결국 파괴되어 불필요한 오버 헤드가 발생합니다. 이것은 자기 할당의 경우 더욱 분명합니다.
  2. 컨테이너에 대한 값 삽입은 복사 초기화와 파기 모두에 무시할 수있는 비용이없는 한 기준을 충족하지 않습니다. 복사 초기화 후 작업이 실패하면 (할당 실패, 중복 값 등) 매개 변수를 삭제해야하므로 불필요한 오버 헤드가 발생합니다.
  3. 매개 변수를 기반으로 한 조건부 객체 생성은 실제로 객체를 생성하지 않을 때 오버 헤드를 발생시킵니다 (예 : std::map::insert_or_assign위의 실패에도 불구하고 컨테이너 삽입).

"무시할 수있는"비용의 정확한 제한은 결국 개발자 및 / 또는 사용자가 허용 할 수있는 비용에 따라 달라지며 경우에 따라 다를 수 있기 때문에 다소 주관적입니다.

실제로, 나는 (보수적으로) 일반적으로 무시할 수있는 비용의 기준을 규정하는 하나의 기계어 (포인터와 같은) 이하의 크기를 가진 사소하게 복사 가능하고 사소한 파괴 가능한 유형을 가정합니다. 결과 코드가 실제로 그러한 경우에 너무 많은 비용이 든다면 빌드 도구의 잘못된 구성이 사용되었거나 도구 체인이 프로덕션 준비가되지 않았 음을 제안합니다.

성능에 더 이상 의심이가는 경우 프로필을 작성하십시오.

추가 사례 연구

규칙에 따라 값으로 전달되거나 전달되지 않는 다른 잘 알려진 유형이 있습니다.

  • 형식은 규칙에 따라 참조 값을 보존해야합니다. 값으로 전달하면 안됩니다.
    • 표준 예는 참조를 전달해야하는 ISO C ++에 정의 된 인수 전달 호출 래퍼 입니다. 호출자 위치에서 ref-qualifier에 대한 참조를 보존 할 수도 있습니다 .
    • 이 예제의 인스턴스는 std::bind입니다. LWG 817 의 해상도를 참조하십시오 .
  • 일부 일반 코드는 일부 매개 변수를 직접 복사 할 수 있습니다. 심지어없이 할 수 std::move의 비용 때문에, 복사본을 무시할 수있는 것으로 가정하고 이동이 반드시 더 잘하지 않습니다.
    • 이러한 매개 변수에는 반복기 및 함수 객체가 포함됩니다 ( 위에서 논의 된 인수 전달 호출자 래퍼 의 경우 제외 ).
    • 의 생성자 템플릿 std::function( 할당 연산자 템플릿은 아님)도 값에 의한 전달 함수 매개 변수를 사용합니다.
  • 무시할 수있는 비용이있는 값에 의한 전달 매개 변수 유형에 필적하는 비용을 갖는 유형도 값에 의한 전달이 선호됩니다. (때로는 전용 대안으로 사용됩니다.) 예를 들어, std::initializer_list인스턴스는 std::basic_string_view두 개의 포인터 또는 포인터에 크기를 더한 것입니다. 이 사실은 참조를 사용하지 않고 직접 전달할 수있을만큼 저렴합니다.
  • 복사본이 필요하지 않는 한 일부 유형은 값으로 전달되는 것을 피하는 것이 좋습니다 . 다른 이유가 있습니다.
    • 복사는 매우 비싸거나 적어도 복사되는 값의 런타임 속성을 검사하지 않고는 복사가 저렴하다는 것을 보장하기가 쉽지 않으므로 기본적으로 복사를 피하십시오. 컨테이너는 이러한 종류의 일반적인 예입니다.
      • 컨테이너에 몇 개의 요소가 있는지 정적으로 알지 못하면 일반적으로 복사 하는 것이 안전 하지 않습니다 ( : DoS 공격 의 의미에서 ).
      • 다른 컨테이너의 중첩 된 컨테이너는 복사 성능 문제를 쉽게 악화시킵니다.
      • 빈 용기라도 저렴하게 복사 할 수 있다는 보장은 없습니다. (엄밀히 말해서, 이것은 컨테이너의 구체적인 구현, 예를 들어 일부 노드 기반 컨테이너에 대한 "sentinel"요소의 존재에 달려 있습니다 ... 그러나 아니요, 단순하게 유지하고 기본적으로 복사를 피하십시오.)
    • Avoid copy by default, even when the performance is totally uninterested, because there can be some unexpected side effects.
      • In particular, allocator-awared containers and some other types with similar treatment to allocators ("container semantics", in David Krauss' word), should not be passed by value - allocator propagation is just another big semantic worm can.
  • A few other types conventionally depend. For example, see GotW #91 for shared_ptr instances. (However, not all smart pointers are like that; observer_ptr are more like raw pointers.)

Pass by value, then move is actually a good idiom for objects that you know are movable.

언급했듯이 rvalue가 전달되면 복사본이 제거되거나 이동 된 다음 생성자 내에서 이동됩니다.

복사 생성자를 오버로드하고 생성자를 명시 적으로 이동할 수 있지만 매개 변수가 두 개 이상이면 더 복잡해집니다.

예를 고려하십시오.

class Obj {
  public:

  Obj(std::vector<int> x, std::vector<int> y)
      : X(std::move(x)), Y(std::move(y)) {}

  private:

  /* Our internal data. */
  std::vector<int> X, Y;

};  // Obj

명시 적 버전을 제공하려는 경우 다음과 같은 4 개의 생성자로 끝납니다.

class Obj {
  public:

  Obj(std::vector<int> &&x, std::vector<int> &&y)
      : X(std::move(x)), Y(std::move(y)) {}

  Obj(std::vector<int> &&x, const std::vector<int> &y)
      : X(std::move(x)), Y(y) {}

  Obj(const std::vector<int> &x, std::vector<int> &&y)
      : X(x), Y(std::move(y)) {}

  Obj(const std::vector<int> &x, const std::vector<int> &y)
      : X(x), Y(y) {}

  private:

  /* Our internal data. */
  std::vector<int> X, Y;

};  // Obj

보시다시피 매개 변수 수를 늘리면 필요한 생성자의 수가 순열에서 증가합니다.

구체적인 유형이 없지만 템플릿 화 된 생성자가있는 경우 다음과 같이 완벽한 전달을 사용할 수 있습니다.

class Obj {
  public:

  template <typename T, typename U>
  Obj(T &&x, U &&y)
      : X(std::forward<T>(x)), Y(std::forward<U>(y)) {}

  private:

  std::vector<int> X, Y;

};   // Obj

참조 :

  1. 속도를 원하십니까? 가치로 전달
  2. C ++ 조미료

나는 몇 가지 대답을 요약하려고 노력할 것이기 때문에 스스로 대답하고 있습니다. 각 경우에 몇 개의 동작 / 사본이 있습니까?

(A) Pass by value and move assignment construct, passing a X parameter. If X is a...

Temporary: 1 move (the copy is elided)

Lvalue: 1 copy 1 move

std::move(lvalue): 2 moves

(B) Pass by reference and copy assignment usual (pre C++11) construct. If X is a...

Temporary: 1 copy

Lvalue: 1 copy

std::move(lvalue): 1 copy

We can assume the three kinds of parameters are equally probable. So every 3 calls we have (A) 4 moves and 1 copy, or (B) 3 copies. I.e., in average, (A) 1.33 moves and 0.33 copies per call or (B) 1 copy per call.

If we come to a situation when our classes consist mostly of PODs, moves are as expensive as copies. So we would have 1.66 copies (or moves) per call to the setter in case (A) and 1 copies in case (B).

We can say that in some circumstances (PODs based types), the pass-by-value-and-then-move construct is a very bad idea. It is 66% slower and it depends on a C++11 feature.

On the other hand, if our classes include containers (which make use of dynamic memory), (A) should be much faster (except if we mostly pass lvalues).

Please, correct me if I'm wrong.


Readability in the declaration:

void foo1( A a ); // easy to read, but unless you see the implementation 
                  // you don't know for sure if a std::move() is used.

void foo2( const A & a ); // longer declaration, but the interface shows
                          // that no copy is required on calling foo().

Performance:

A a;
foo1( a );  // copy + move
foo2( a );  // pass by reference + copy

Responsibilities:

A a;
foo1( a );  // caller copies, foo1 moves
foo2( a );  // foo2 copies

For typical inline code there is usually no difference when optimized. But foo2() might do the copy only on certain conditions (e.g. insert into map if key does not exist), whereas for foo1() the copy will always be done.

참고URL : https://stackoverflow.com/questions/21035417/is-the-pass-by-value-and-then-move-construct-a-bad-idiom

반응형