initializer_list 및 이동 의미 체계


97

요소를 외부로 이동할 수 std::initializer_list<T>있습니까?

#include <initializer_list>
#include <utility>

template<typename T>
void foo(std::initializer_list<T> list)
{
    for (auto it = list.begin(); it != list.end(); ++it)
    {
        bar(std::move(*it));   // kosher?
    }
}

때문에 std::intializer_list<T>특별한 컴파일러주의가 필요하며 C ++ 표준 라이브러리의 일반 컨테이너와 같은 값의 의미가없는, 내가 오히려 미안한 것보다 안전한 물어 것입니다.


객체가에 의해 참조하는 핵심 언어를 정의가 initializer_list<T>있는 -const. 마찬가지로 객체를 initializer_list<int>나타냅니다 int. 하지만 이것이 결함이라고 생각합니다. 컴파일러가 읽기 전용 메모리에 목록을 정적으로 할당 할 수 있다는 것입니다.
Johannes Schaub-litb 2011

답변:


89

아니요, 의도 한대로 작동하지 않습니다. 당신은 여전히 ​​사본을 얻을 것입니다. 나는 이것이 'd' initializer_list가 될 때까지 일시적인 배열을 유지하기 위해 존재 한다고 생각했기 때문에 꽤 놀랐습니다 move.

beginend에 대한 initializer_list반환 const T *의 결과 때문에 move코드에서입니다 T const &&- 불변를 rvalue 참조. 그러한 표현은 의미있게 이동할 수 없습니다. T const &rvalue는 const lvalue 참조에 바인딩되기 때문에 형식의 함수 매개 변수에 바인딩되며 여전히 복사 의미 체계를 볼 수 있습니다.

아마도 그 이유는 컴파일러가 initializer_list정적으로 초기화 된 상수 를 만들도록 선택할 수 있기 때문일 것입니다. 그러나 유형을 만드는 것이 더 깨끗 initializer_list하거나 const initializer_list컴파일러의 재량에 따라 사용자가 어떤 것을 기대할 것인지 const변경 가능할 것인지 알지 못합니다. begin및의 결과입니다 end. 그러나 그것은 단지 내 직감입니다. 아마도 내가 틀린 이유가있을 것입니다.

업데이트 : 이동 전용 유형 지원을 위한 ISO 제안 을 작성 했습니다initializer_list . 초안 일 뿐이며 아직 어디에도 구현되지 않았지만 문제에 대한 자세한 분석을 위해 볼 수 있습니다.


11
명확 std::move하지 않은 경우에도 생산적이지 않더라도 안전한 사용을 의미 합니다. ( T const&&이동 생성자 금지 )
Luc Danton 2011

나는 당신이 전체적인 논쟁을 할 수 있다고 생각하지 않습니다. const std::initializer_list<T>또는 그저 std::initializer_list<T>놀라움을 자주 일으키지 않는 방식으로 말입니다. 고려의 각 인수는 것이 initializer_list될 수 있습니다 const여부와 그 호출자의 맥락에서 알려져 있지만 내부 컴파일러는 호출자의 컨텍스트 (즉,의 코드의 한 버전을 생성해야한다 foo는 주장에 대해 아무것도 모르는 발신자가 전달하고 있음)
David Rodríguez-dribeas

1
@David : 좋은 지적이지만 std::initializer_list &&, 참조가 아닌 오버로드가 필요한 경우에도 오버로드가 무언가를 수행 하도록하는 것이 여전히 유용 합니다. 이미 안 좋은 현재 상황보다 훨씬 더 혼란 스러울 것 같습니다.
Potatoswatter

1
@JBJansen 해킹 당할 수 없습니다. 나는 그 코드가 wrt initializer_list를 수행하기 위해 무엇을해야하는지 정확히 알지 못하지만, 사용자로서 당신은 그것에서 이동하는 데 필요한 권한이 없습니다. 안전한 코드는 그렇게하지 않습니다.
Potatoswatter

1
@ Potatoswatter, 늦은 댓글이지만 제안 상태는 어떻습니까? C ++ 20에 들어갈 수있는 먼 기회가 있습니까?
WhiZTiM

20
bar(std::move(*it));   // kosher?

당신이 의도 한 방식이 아닙니다. const개체를 이동할 수 없습니다 . 그리고 해당 요소에 std::initializer_list대한 const액세스 만 제공합니다 . 따라서 유형은 it입니다 const T *.

호출 std::move(*it)을 시도 하면 l- 값만 생성됩니다. IE : 사본.

std::initializer_list정적 메모리를 참조 합니다. 그것이 수업의 목적입니다. 움직임 은 그것을 변경하는 것을 의미하기 때문에 정적 메모리에서 이동할 수 없습니다 . 당신은 그것에서만 복사 할 수 있습니다.


const xvalue는 여전히 xvalue initializer_list이며 필요한 경우 스택을 참조합니다. (내용이 일정하지 않더라도 스레드로부터 안전합니다.)
Potatoswatter

5
@Potatoswatter : 상수 개체에서 이동할 수 없습니다. initializer_list객체가 xValue 자체가 될 수 있지만, 컨텐츠 (그것이 가리키는 지 값의 실제 배열)되어있어 const그 내용이 고정 값이 될 수 있기 때문에. 의 내용에서 이동할 수는 없습니다 initializer_list.
Nicol Bolas 2011

내 대답과 토론을 참조하십시오. 그는 참조 해제 된 반복자를 이동하여 constx 값을 생성했습니다 . move의미가 없을 수도 있지만, 그것을 받아들이는 매개 변수를 선언하는 것은 합법적이며 심지어 가능합니다. 특정 유형의 이동이 작동하지 않는 경우 올바르게 작동 할 수도 있습니다.
Potatoswatter 2011

1
@Potatoswatter : C ++ 11 표준은 .NET을 사용하지 않는 한 임시가 아닌 객체가 실제로 이동되지 않도록 많은 언어를 사용 std::move합니다. 이렇게하면 이동 작업이 발생했을 때 소스와 대상 모두에 영향을 미치기 때문에 검사를 통해 알 수 있습니다 (명명 된 객체에 대해 암시 적으로 발생하지 않기를 원함). 따라서 std::move이동 작업 발생 하지 않는 장소에서 사용하는 경우 (x 값이 있으면 실제 이동이 발생하지 const않음) 코드가 잘못된 것입니다. 객체에서 std::move호출 가능하다는 것은 실수라고 생각 const합니다.
Nicol Bolas 2011

1
아마도, 오해의 소지가있는 코드의 가능성에 대한 규칙에 대한 예외는 더 적을 것입니다. 어쨌든, 그것이 합법적 임에도 불구하고 내가 "아니오"라고 대답 한 이유이며, 그 결과는 const lvalue로만 바인드하더라도 xvalue입니다. 솔직히 말해서 const &&관리 포인터가있는 가비지 수집 클래스에서 이미 간단한 유혹을 받았는데 , 관련된 모든 것이 변경 가능하고 이동이 포인터 관리를 이동했지만 포함 된 값에는 영향을주지 않았습니다. 항상 까다로운 경우가 있습니다. : v).
Potatoswatter 2011

2

이것은 list.begin()유형이 있기 때문에 명시된대로 작동하지 않으며 const T *상수 객체에서 이동할 수있는 방법이 없습니다. 언어 디자이너는 아마도 초기화 목록에 예를 들어 문자열 상수를 포함 할 수 있도록 그렇게 만들었을 것입니다.

그러나 이니셜 라이저 목록에 rvalue 표현식이 포함되어 있다는 것을 알고있는 경우 (또는 사용자가이를 작성하도록 강제하려는 경우) 작동하도록하는 트릭이 있습니다 (Sumant의 답변에서 영감을 얻었습니다. 그러나 해결책은 그보다 훨씬 간단합니다). 이니셜 라이저 목록에 저장된 요소는 T값이 아니라을 캡슐화하는 값 이어야 합니다 T&&. 그런 다음 해당 값 자체가 const한정된 경우에도 수정 가능한 rvalue를 검색 할 수 있습니다.

template<typename T>
  class rref_capture
{
  T* ptr;
public:
  rref_capture(T&& x) : ptr(&x) {}
  operator T&& () const { return std::move(*ptr); } // restitute rvalue ref
};

이제 initializer_list<T>인수 를 선언하는 대신 인수를 선언합니다 initializer_list<rref_capture<T> >. 다음은 std::unique_ptr<int>이동 의미론 만 정의 된 스마트 포인터 벡터를 포함하는 구체적인 예입니다 (따라서 이러한 객체 자체는 초기화 목록에 저장 될 수 없습니다). 그러나 아래 이니셜 라이저 목록은 문제없이 컴파일됩니다.

#include <memory>
#include <initializer_list>
class uptr_vec
{
  typedef std::unique_ptr<int> uptr; // move only type
  std::vector<uptr> data;
public:
  uptr_vec(uptr_vec&& v) : data(std::move(v.data)) {}
  uptr_vec(std::initializer_list<rref_capture<uptr> > l)
    : data(l.begin(),l.end())
  {}
  uptr_vec& operator=(const uptr_vec&) = delete;
  int operator[] (size_t index) const { return *data[index]; }
};

int main()
{
  std::unique_ptr<int> a(new int(3)), b(new int(1)),c(new int(4));
  uptr_vec v { std::move(a), std::move(b), std::move(c) };
  std::cout << v[0] << "," << v[1] << "," << v[2] << std::endl;
}

한 가지 질문에 답이 필요합니다. 이니셜 라이저 목록의 요소가 참 prvalue (예 : xvalue) 여야하는 경우 언어는 해당 임시의 수명이 사용되는 지점까지 확장되도록 보장합니까? 솔직히, 표준의 관련 섹션 8.5가이 문제를 전혀 다루지 않는다고 생각합니다. 그러나 1.9 : 10을 읽으면 모든 경우에 관련된 full-expression 이 이니셜 라이저 목록의 사용을 포함하는 것처럼 보이 므로 rvalue 참조를 매달릴 위험이 없다고 생각합니다.


문자열 상수? 처럼 "Hello world"? 그것들에서 이동하면 포인터를 복사 (또는 참조를 바인딩)하기 만하면됩니다.
dyp

1
"하나의 질문에 답이 필요합니다" 내부 이니셜 라이저 {..}는의 함수 매개 변수에있는 참조에 바인딩됩니다 rref_capture. 이것은 그들의 수명을 연장하지 않으며, 생성 된 완전한 표현이 끝날 때 여전히 파괴됩니다.
dyp

TC 다른 답변에서의 코멘트 : 생성자의 여러 오버로드가있는 경우, 포장 std::initializer_list<rref_capture<T>>- 말하자면, 사용자가 선택한 일부 변환 특성에 std::decay_t원치 않는 공제를 차단 -.
분석 재개 모니카

2

해결 방법에 대한 합리적인 시작점을 제공하는 것이 유익 할 것이라고 생각했습니다.

주석 인라인.

#include <memory>
#include <vector>
#include <array>
#include <type_traits>
#include <algorithm>
#include <iterator>

template<class Array> struct maker;

// a maker which makes a std::vector
template<class T, class A>
struct maker<std::vector<T, A>>
{
  using result_type = std::vector<T, A>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const -> result_type
  {
    result_type result;
    result.reserve(sizeof...(Ts));
    using expand = int[];
    void(expand {
      0,
      (result.push_back(std::forward<Ts>(ts)),0)...
    });

    return result;
  }
};

// a maker which makes std::array
template<class T, std::size_t N>
struct maker<std::array<T, N>>
{
  using result_type = std::array<T, N>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const
  {
    return result_type { std::forward<Ts>(ts)... };
  }

};

//
// delegation function which selects the correct maker
//
template<class Array, class...Ts>
auto make(Ts&&...ts)
{
  auto m = maker<Array>();
  return m(std::forward<Ts>(ts)...);
}

// vectors and arrays of non-copyable types
using vt = std::vector<std::unique_ptr<int>>;
using at = std::array<std::unique_ptr<int>,2>;


int main(){
    // build an array, using make<> for consistency
    auto a = make<at>(std::make_unique<int>(10), std::make_unique<int>(20));

    // build a vector, using make<> because an initializer_list requires a copyable type  
    auto v = make<vt>(std::make_unique<int>(10), std::make_unique<int>(20));
}

문제는 initializer_list누군가가 해결 방법을 가지고 있는지 여부가 아니라에서 이동할 수 있는지 여부였습니다. 게다가의 주요 판매 포인트 initializer_list는 요소 수가 아니라 요소 유형에 대해서만 템플릿이 지정되므로 수신자도 템플릿을 만들 필요가 없다는 것입니다.
underscore_d

1
@underscore_d 당신이 절대적으로 옳습니다. 나는 질문과 관련된 지식을 공유하는 것이 그 자체로 좋은 것이라고 생각합니다. 이 경우, 아마도 그것은 OP에 도움이되었고 아마도 그렇지 않았을 것입니다. 그는 응답하지 않았습니다. 그러나 종종 OP와 다른 사람들은 질문과 관련된 추가 자료를 환영합니다.
Richard Hodges

물론, 그것은 같은 것을 원 initializer_list하지만 그것을 유용하게 만드는 모든 제약을받지 않는 독자들에게 실제로 도움이 될 수 있습니다 . :)
underscore_d

@underscore_d 내가 간과 한 제약은 무엇입니까?
Richard Hodges

내가 의미하는 것은 initializer_list(컴파일러 마법을 통해) 요소 수에 대한 템플릿 함수, 즉 배열 및 / 또는 가변 함수를 기반으로하는 대안에서 본질적으로 필요한 것을 피하여 후자가 사용 가능한 경우 범위를 제한한다는 것입니다. 내 이해에 따르면 이것은를 갖는 주요 근거 중 하나 initializer_list이므로 언급 할 가치가있는 것 같습니다.
underscore_d

0

이미 답변 한대로 현재 표준에서는 허용되지 않는 것 같습니다 . 다음은 이니셜 라이저 목록을 사용하는 대신 가변으로 함수를 정의하여 유사한 작업을 수행하는 또 다른 해결 방법입니다.

#include <vector>
#include <utility>

// begin helper functions

template <typename T>
void add_to_vector(std::vector<T>* vec) {}

template <typename T, typename... Args>
void add_to_vector(std::vector<T>* vec, T&& car, Args&&... cdr) {
  vec->push_back(std::forward<T>(car));
  add_to_vector(vec, std::forward<Args>(cdr)...);
}

template <typename T, typename... Args>
std::vector<T> make_vector(Args&&... args) {
  std::vector<T> result;
  add_to_vector(&result, std::forward<Args>(args)...);
  return result;
}

// end helper functions

struct S {
  S(int) {}
  S(S&&) {}
};

void bar(S&& s) {}

template <typename T, typename... Args>
void foo(Args&&... args) {
  std::vector<T> args_vec = make_vector<T>(std::forward<Args>(args)...);
  for (auto& arg : args_vec) {
    bar(std::move(arg));
  }
}

int main() {
  foo<S>(S(1), S(2), S(3));
  return 0;
}

Variadic 템플릿은 initializer_list와 달리 r- 값 참조를 적절하게 처리 할 수 ​​있습니다.

이 예제 코드에서는 작은 도우미 함수 세트를 사용하여 가변 인수를 벡터로 변환하여 원본 코드와 유사하게 만들었습니다. 그러나 물론 가변 템플릿을 사용하여 재귀 함수를 직접 작성할 수 있습니다.


문제는 initializer_list누군가가 해결 방법을 가지고 있는지 여부가 아니라에서 이동할 수 있는지 여부였습니다. 게다가의 주요 판매 포인트 initializer_list는 요소 수가 아니라 요소 유형에 대해서만 템플릿이 지정되므로 수신자도 템플릿을 만들 필요가 없다는 것입니다.
underscore_d

0

요소 이동의 의도를 표시하는 태그 역할을하는 래퍼 클래스를 사용하는 훨씬 간단한 구현이 있습니다. 이것은 컴파일 시간 비용입니다.

래퍼 클래스는 방법으로 사용하도록 설계되어 std::move단지 대체 사용 std::move으로 move_wrapper, 그러나 이것은 C ++ 17이 필요합니다. 이전 사양의 경우 추가 빌더 방법을 사용할 수 있습니다.

내부에 래퍼 클래스를 허용 initializer_list하고 그에 따라 요소를 이동하는 빌더 메서드 / 생성자를 작성해야합니다 .

일부 요소를 이동하는 대신 복사해야하는 경우에 전달하기 전에 사본을 구성하십시오 initializer_list.

코드는 자체 문서화되어야합니다.

#include <iostream>
#include <vector>
#include <initializer_list>

using namespace std;

template <typename T>
struct move_wrapper {
    T && t;

    move_wrapper(T && t) : t(move(t)) { // since it's just a wrapper for rvalues
    }

    explicit move_wrapper(T & t) : t(move(t)) { // acts as std::move
    }
};

struct Foo {
    int x;

    Foo(int x) : x(x) {
        cout << "Foo(" << x << ")\n";
    }

    Foo(Foo const & other) : x(other.x) {
        cout << "copy Foo(" << x << ")\n";
    }

    Foo(Foo && other) : x(other.x) {
        cout << "move Foo(" << x << ")\n";
    }
};

template <typename T>
struct Vec {
    vector<T> v;

    Vec(initializer_list<T> il) : v(il) {
    }

    Vec(initializer_list<move_wrapper<T>> il) {
        v.reserve(il.size());
        for (move_wrapper<T> const & w : il) {
            v.emplace_back(move(w.t));
        }
    }
};

int main() {
    Foo x{1}; // Foo(1)
    Foo y{2}; // Foo(2)

    Vec<Foo> v{Foo{3}, move_wrapper(x), Foo{y}}; // I want y to be copied
    // Foo(3)
    // copy Foo(2)
    // move Foo(3)
    // move Foo(1)
    // move Foo(2)
}

0

를 사용하는 대신 std::initializer_list<T>인수를 배열 rvalue 참조로 선언 할 수 있습니다.

template <typename T>
void bar(T &&value);

template <typename T, size_t N>
void foo(T (&&list)[N] ) {
   std::for_each(std::make_move_iterator(std::begin(list)),
                 std::make_move_iterator(std::end(list)),
                 &bar);
}

void baz() {
   foo({std::make_unique<int>(0), std::make_unique<int>(1)});
}

https://gcc.godbolt.org/z/2uNxv6 를 사용하는 예를 참조하십시오 std::unique_ptr<int>.


-1

cpptruths에in<T> 설명 된 관용구를 고려하십시오 . 아이디어는 런타임에 lvalue / rvalue를 결정한 다음 move 또는 copy-construction을 호출하는 것입니다. initializer_list에서 제공하는 표준 인터페이스가 const 참조 인 경우에도 rvalue / lvalue를 감지합니다.in<T>


4
컴파일러가 이미 알고 있는데 왜 런타임에 값 범주를 결정하고 싶습니까?
fredoverflow

1
동의하지 않거나 더 나은 대안이 있으면 블로그를 읽고 의견을 남겨주세요. 컴파일러가 값 범주를 알고 있더라도 initializer_list는 상수 반복자 만 있기 때문에이를 보존하지 않습니다. 따라서 initializer_list를 구성 할 때 값 범주를 "캡처"하고 함수가 원하는대로 사용할 수 있도록 전달해야합니다.
Sumant 2013 년

5
이 답변은 기본적으로 링크를 따르지 않고 쓸모가 없으며 SO 답변은 링크를 따르지 않고 유용해야합니다.
Yakk-Adam Nevraumont

1
@Sumant [다른 곳에서 동일한 게시물에서 내 의견을 복사] 그 엄청난 혼란이 실제로 성능이나 메모리 사용에 측정 가능한 이점을 제공합니까? 무엇을하려고하는지 알아내는 데 한 시간 정도 걸리나요? 의심 스럽습니다.
underscore_d
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.