C ++ 맵에서 insert vs emplace vs operator []


191

처음으로 맵을 사용하고 있으며 요소를 삽입하는 방법이 많이 있음을 깨달았습니다. 당신은 사용할 수 있습니다 emplace(), operator[]또는 insert()플러스 사용과 같은 변형 value_type또는 make_pair. 모든 사례에 대한 많은 정보와 특정 사례에 대한 질문이 있지만 여전히 큰 그림을 이해할 수 없습니다. 그래서 두 가지 질문은 다음과 같습니다.

  1. 다른 것보다 그들 각각의 장점은 무엇입니까?

  2. 표준에 배치를 추가해야합니까? 그것 없이는 불가능했던 것이 있습니까?


1
배치 시맨틱은 명시적인 변환 및 직접 초기화를 허용합니다.
Kerrek SB 2016

3
이제는를 operator[]기반으로 try_emplace합니다. 언급 할 가치가있을 수도 있습니다 insert_or_assign.
FrankHB

@FrankHB 당신 (또는 다른 사람)이 최신 답변을 추가하면 허용되는 답변을 변경할 수 있습니다.
German Capuano

답변:


228

:지도의 특정 경우 기존의 옵션은 두 가지였다 operator[]insert(의 다른 맛을 insert). 그래서 나는 그것들을 설명하기 시작할 것입니다.

operator[]A는 발견 또는-추가 연산자. 지도 안에서 주어진 키를 가진 요소를 찾으려고 시도하고 존재하는 경우 저장된 값에 대한 참조를 반환합니다. 그렇지 않은 경우 기본 초기화로 새 요소가 삽입되어 참조를 반환합니다.

insert(단일 소자 풍미) 함수는 소요 value_type( std::pair<const Key,Value>)는 키 (사용 first부재)와 삽입하려고. 때문에 std::map중복을 허용하지 않는 것도 삽입은하지 않습니다 기존 요소가있는 경우.

이 둘의 첫 번째 차이점은 operator[]기본 초기화 을 구성 할 수 있어야한다는 점입니다. 따라서 기본 초기화 할 수없는 값 유형에는 사용할 수 없습니다. 둘 사이의 두 번째 차이점은 주어진 키를 가진 요소가 이미있을 때 발생하는 것입니다. 이 insert함수는 맵의 상태를 수정하지 않고 반복자를 요소 (및 false삽입되지 않았 음을 나타내는)에 반환합니다 .

// assume m is std::map<int,int> already has an element with key 5 and value 0
m[5] = 10;                      // postcondition: m[5] == 10
m.insert(std::make_pair(5,15)); // m[5] is still 10

insert인수 의 경우의 객체이며 value_type다른 방식으로 만들 수 있습니다. 적절한 유형으로 직접 구성하거나 대상을 만들 value_type수있는 대상을 전달할 수 있습니다. 이는 대상 std::make_pair을 간단하게 만들 수 std::pair있지만 원하는 것은 아닙니다.

다음 호출의 순 효과는 비슷합니다 .

K t; V u;
std::map<K,V> m;           // std::map<K,V>::value_type is std::pair<const K,V>

m.insert( std::pair<const K,V>(t,u) );      // 1
m.insert( std::map<K,V>::value_type(t,u) ); // 2
m.insert( std::make_pair(t,u) );            // 3

그러나 실제로는 같지 않습니다 ... [1]과 [2]는 실제로 동일합니다. 두 경우 모두 코드는 동일한 유형 ( std::pair<const K,V>) 의 임시 객체를 만들어 insert함수에 전달합니다 . 이 insert함수는 이진 검색 트리에 적절한 노드를 만든 다음 value_type인수에서 노드로 부분 을 복사 합니다. 사용의 장점은 항상 일치value_type 한다는 것 입니다. 인수 의 유형을 잘못 입력 할 수 없습니다 !value_type value_typestd::pair

차이점은 [3]에 있습니다. 이 함수 std::make_pair는를 생성하는 템플릿 함수입니다 std::pair. 서명은 다음과 같습니다

template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );

나는 std::make_pair일반적인 사용법이므로 의도적으로 템플릿 인수를 제공하지 않았습니다 . 그리고 의미는 템플릿 인수로이 경우, 호출에서 추론하는 것입니다 T==K,U==V호출 할 수 있도록 std::make_pair반환하며 std::pair<K,V>(누락 참고 const). 서명은 value_type가깝지만 호출에서 반환 된 값과 동일하지 않아야합니다 std::make_pair. 충분히 가깝기 때문에 올바른 유형의 임시를 작성하고 복사를 초기화합니다. 그러면 노드에 복사되어 총 2 개의 사본이 생성됩니다.

템플릿 인수를 제공하면이 문제를 해결할 수 있습니다.

m.insert( std::make_pair<const K,V>(t,u) );  // 4

그러나 [1]의 경우 명시 적으로 유형을 입력하는 것과 같은 방식으로 오류가 발생하기 쉽습니다.

지금까지 외부 insert에서 생성 value_type하고 해당 객체를 컨테이너에 복사 해야하는 다른 호출 방법 이 있습니다. 또는 operator[]유형이 기본 구성 가능 하고 할당 가능한 경우 (의도적으로 만 초점을 맞추고 m[k]=v) 한 객체의 기본 초기화와 해당 객체 에 대한 값 의 사본 이 필요한 경우 사용할 수 있습니다.

11 C ++에서, 가변 템플릿 완벽한 전송로를 이용하여 용기에 요소를 추가하는 새로운 방법이 emplacing (장소에서 생성). emplace다른 컨테이너 의 함수는 기본적으로 동일한 기능을 수행합니다. 컨테이너 에 복사소스 를 가져 오는 대신 컨테이너에 저장된 객체의 생성자로 전달되는 매개 변수를 사용합니다.

m.emplace(t,u);               // 5

[5]에서는로 std::pair<const K, V>작성되어 전달 emplace되지 않고 tu오브젝트에 대한 참조 가 전달되어 데이터 구조 내의 하위 오브젝트 emplace의 생성자로 전달됩니다 value_type. 이 경우의 사본 이 전혀std::pair<const K,V> 수행 되지 않으므로emplace C ++ 03 대안 에 비해 이점이 있습니다. 의 경우와 마찬가지로 insert맵의 값을 무시하지 않습니다.


내가 생각하지 않은 흥미로운 질문 emplace은 실제로지도를 구현 하는 방법 이며, 일반적인 경우에는 간단한 문제가 아닙니다.


5
이것은 대답에서 암시되지만 map [] = val은 이전 값이 있으면 덮어 씁니다.
dk123

내 생각에 더 흥미로운 질문은 그것이 거의 목적을 달성하지 못한다는 것입니다. 페어 카피를 저장하기 때문에 페어 카피가 없으면 복사가 없기 때문에 좋습니다 mapped_type. 우리가 원하는 mapped_type것은 쌍의 구성을 배치하고 쌍 구성을 맵에 배치하는 것입니다. 따라서 std::pair::emplace기능과 전달 기능 map::emplace이 모두 누락되었습니다. 현재 양식에서는 여전히 페어 생성자에 생성 된 map_type을 제공해야합니다. 두 번보다 좋지만 여전히 좋지 않습니다.
v.oddou

실제로 나는 그 의견을 수정한다. C ++ 11에는 1 개의 인수 구성의 경우와 같은 목적을 제공하는 템플릿 쌍 생성자가있다. 그리고 튜플을 사용하여 인수를 전달하는 이상한 조각 단위 구성을 통해 전달을 완벽하게 전달할 수 있습니다.
v.oddou

unordered_map 및 map에 insert 성능 오류가있는 것 같습니다 : link
Deqing

1
에 대한 정보와이 업데이트 좋을 수도 insert_or_assigntry_emplace도움이 기존의 방법에서 기능에 약간의 간격을 채우기 (17 ++ C에서 모두).
ShadowRanger

15

Emplace : rvalue 참조를 활용하여 이미 생성 한 실제 객체를 사용합니다. 즉, 복사 또는 이동 생성자가 호출되지 않으며 LARGE 객체에 적합합니다! O (로그 (N)) 시간

삽입 : 표준 lvalue 참조 및 rvalue 참조에 대한 오버로드와 삽입 할 요소 목록에 대한 반복자 및 요소가 속한 위치에 대한 "힌트"가 있습니다. "힌트"반복자를 사용하면 삽입 시간이 일정한 시간으로 줄어 듭니다. 그렇지 않으면 O (log (N)) 시간입니다.

Operator [] : 객체가 존재하는지 확인하고 존재하는 경우이 객체에 대한 참조를 수정하고, 그렇지 않으면 제공된 키와 값을 사용하여 두 객체에 대해 make_pair를 호출 한 다음 삽입 기능과 동일한 작업을 수행합니다. 이것은 O (log (N)) 시간입니다.

make_pair : 쌍을 만드는 것 이상을 수행하지 않습니다.

표준에 배치를 추가 할 필요가 없습니다. c ++ 11에서는 && 유형의 참조가 추가되었다고 생각합니다. 이것은 이동 의미론의 필요성을 제거하고 특정 유형의 메모리 관리를 최적화 할 수있게했습니다. 특히 rvalue 참조입니다. 오버로드 된 insert (value_type &&) 연산자는 in_place 시맨틱을 이용하지 않으므로 훨씬 덜 효율적입니다. rvalue 참조를 처리 할 수있는 기능을 제공하지만 객체의 구성에 대한 주요 목적은 무시합니다.


4
" 표준에 자리를 추가 할 필요가 없습니다." 이것은 특허 적으로 거짓입니다. emplace()복사하거나 이동할 수없는 요소를 삽입하는 유일한 방법입니다. 그것은 "하지 약 : 또한 당신이 아이디어 잘못있어 보인다 (그런 일이있는 경우 및 예, 아마, 가장 효율적으로 한 삽입 복사 및 이동 생성자는 더 건설보다 훨씬 비용) 를 rvalue 참조의 [고려] 장점 이미 만든 실제 개체를 사용하려면 "; 어떤 객체는 아직 생성되지 않습니다, 당신은 앞으로 map인수 자체 내부를 만들어야합니다. 당신은 물건을 만들지 않습니다.
underscore_d

10

최적화 기회와 간단한 구문 외에도 삽입과 배치 사이의 중요한 차이점은 후자가 명시 적 변환을 허용한다는 것 입니다. (이것은지도뿐만 아니라 전체 표준 라이브러리에 있습니다.)

다음은 시연하는 예입니다.

#include <vector>

struct foo
{
    explicit foo(int);
};

int main()
{
    std::vector<foo> v;

    v.emplace(v.end(), 10);      // Works
    //v.insert(v.end(), 10);     // Error, not explicit
    v.insert(v.end(), foo(10));  // Also works
}

이것은 매우 구체적인 세부 사항이지만, 사용자 정의 전환 체인을 다루는 경우이를 염두에 두어야합니다.


foo가 ctor에 하나가 아닌 두 개의 정수가 필요하다고 상상해보십시오. 이 통화를 사용할 수 있습니까? v.emplace(v.end(), 10, 10); ... 또는 이제 다음을 사용해야 v.emplace(v.end(), foo(10, 10) ); 합니까?
Kaitain

지금은 컴파일러에 액세스 할 수 없지만 두 버전이 모두 작동한다고 가정합니다. 거의 모든 예제 emplace는 단일 매개 변수를 사용하는 클래스를 사용합니다. IMO는 실제로 여러 매개 변수가 예제에 사용 된 경우 emplace의 variadic 구문의 특성을 훨씬 더 명확하게 만듭니다.
Kaitain

9

다음 코드는 어떻게 insert()다른지에 대한 "큰 그림 아이디어"를 이해하는 데 도움이 될 수 있습니다 emplace().

#include <iostream>
#include <unordered_map>
#include <utility>

//Foo simply outputs what constructor is called with what value.
struct Foo {
  static int foo_counter; //Track how many Foo objects have been created.
  int val; //This Foo object was the val-th Foo object to be created.

  Foo() { val = foo_counter++;
    std::cout << "Foo() with val:                " << val << '\n';
  }
  Foo(int value) : val(value) { foo_counter++;
    std::cout << "Foo(int) with val:             " << val << '\n';
  }
  Foo(Foo& f2) { val = foo_counter++;
    std::cout << "Foo(Foo &) with val:           " << val
              << " \tcreated from:      \t" << f2.val << '\n';
  }
  Foo(const Foo& f2) { val = foo_counter++;
    std::cout << "Foo(const Foo &) with val:     " << val
              << " \tcreated from:      \t" << f2.val << '\n';
  }
  Foo(Foo&& f2) { val = foo_counter++;
    std::cout << "Foo(Foo&&) moving:             " << f2.val
              << " \tand changing it to:\t" << val << '\n';
  }
  ~Foo() { std::cout << "~Foo() destroying:             " << val << '\n'; }

  Foo& operator=(const Foo& rhs) {
    std::cout << "Foo& operator=(const Foo& rhs) with rhs.val: " << rhs.val
              << " \tcalled with lhs.val = \t" << val
              << " \tChanging lhs.val to: \t" << rhs.val << '\n';
    val = rhs.val;
    return *this;
  }

  bool operator==(const Foo &rhs) const { return val == rhs.val; }
  bool operator<(const Foo &rhs)  const { return val < rhs.val;  }
};

int Foo::foo_counter = 0;

//Create a hash function for Foo in order to use Foo with unordered_map
namespace std {
   template<> struct hash<Foo> {
       std::size_t operator()(const Foo &f) const {
           return std::hash<int>{}(f.val);
       }
   };
}

int main()
{
    std::unordered_map<Foo, int> umap;  
    Foo foo0, foo1, foo2, foo3;
    int d;

    //Print the statement to be executed and then execute it.

    std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n";
    umap.insert(std::pair<Foo, int>(foo0, d));
    //Side note: equiv. to: umap.insert(std::make_pair(foo0, d));

    std::cout << "\numap.insert(std::move(std::pair<Foo, int>(foo1, d)))\n";
    umap.insert(std::move(std::pair<Foo, int>(foo1, d)));
    //Side note: equiv. to: umap.insert(std::make_pair(foo1, d));

    std::cout << "\nstd::pair<Foo, int> pair(foo2, d)\n";
    std::pair<Foo, int> pair(foo2, d);

    std::cout << "\numap.insert(pair)\n";
    umap.insert(pair);

    std::cout << "\numap.emplace(foo3, d)\n";
    umap.emplace(foo3, d);

    std::cout << "\numap.emplace(11, d)\n";
    umap.emplace(11, d);

    std::cout << "\numap.insert({12, d})\n";
    umap.insert({12, d});

    std::cout.flush();
}

내가 얻은 결과는 다음과 같습니다.

Foo() with val:                0
Foo() with val:                1
Foo() with val:                2
Foo() with val:                3

umap.insert(std::pair<Foo, int>(foo0, d))
Foo(Foo &) with val:           4    created from:       0
Foo(Foo&&) moving:             4    and changing it to: 5
~Foo() destroying:             4

umap.insert(std::move(std::pair<Foo, int>(foo1, d)))
Foo(Foo &) with val:           6    created from:       1
Foo(Foo&&) moving:             6    and changing it to: 7
~Foo() destroying:             6

std::pair<Foo, int> pair(foo2, d)
Foo(Foo &) with val:           8    created from:       2

umap.insert(pair)
Foo(const Foo &) with val:     9    created from:       8

umap.emplace(foo3, d)
Foo(Foo &) with val:           10   created from:       3

umap.emplace(11, d)
Foo(int) with val:             11

umap.insert({12, d})
Foo(int) with val:             12
Foo(const Foo &) with val:     13   created from:       12
~Foo() destroying:             12

~Foo() destroying:             8
~Foo() destroying:             3
~Foo() destroying:             2
~Foo() destroying:             1
~Foo() destroying:             0
~Foo() destroying:             13
~Foo() destroying:             11
~Foo() destroying:             5
~Foo() destroying:             10
~Foo() destroying:             7
~Foo() destroying:             9

그것을주의해라:

  1. unordered_map항상 내부적으로 Foo객체 ( Foo *s가 아닌 s)를 키로 저장하며, 키는 파괴 될 때 모두 파괴됩니다 unordered_map. 여기서 unordered_map의 내부 키는 foos 13, 11, 5, 10, 7, 9입니다.

    • 따라서 기술적으로 unordered_map실제로 std::pair<const Foo, int>객체를 저장하고 Foo객체를 저장 합니다. 그러나 어떻게 emplace()다른지에 대한 "큰 그림 아이디어"를 이해하려면 insert()(아래 강조 표시된 상자 참조) 이 객체 를 일시적std::pair 으로 완전히 수동적 인 것으로 상상 해도됩니다 . 이 "큰 그림 아이디어"를 이해하고 나면 미묘하지만 중요한 기술 std::pairunordered_map도입 하여이 중개 대상 의 사용 방법을 백업하고 이해하는 것이 중요합니다.
  2. 각각의 삽입 foo0, foo1그리고 foo2중 하나에 2 호출 요구 Foo의 복사 / 이동 생성자와 2 개 통화 Foo'소멸자의 (지금 설명대로) :

    ㅏ. 각각 삽입 foo0하고 foo1(임시 객체 생성 foo4하고 foo6, 그 소멸 삽입이 완료된 후 즉시 호출 된 각각 등). 또한, unorder_map의 내부 Foo( Foos 5와 7)는 unorder_map이 파괴 될 때 소멸자를 호출했습니다.

    비. 삽입하기 위해 foo2먼저 임시 쌍이 아닌 객체 ( pair)를 명시 적으로 만들었습니다.이 객체 는의 내부 Foo생성자로 foo2생성 foo8되는 의 복사 생성자 를 호출했습니다 pair. 그런 다음 insert()이 쌍을 수정 unordered_map하여 사본 생성자를 다시 호출하여 (on foo8) 자체 내부 사본 ( foo9) 을 작성했습니다 . 와 같이 fooS 0과 1 최종 결과는 해당되는 차이점이 삽입 두 소멸자 호출이었다 foo8우리의 끝에 도달 할 때의 소멸자 만 불렸다 main()보다는 후 즉시 호출되는 insert()마쳤다.

  3. 삽입 foo3하면 1 개의 복사 / 이동 생성자 호출 ( foo10내부적으로 작성 unordered_map)과 Foo소멸자 에 대한 호출 1 개만 발생했습니다 . (나중에 다시 설명하겠습니다).

  4. 의 경우 실행이 메서드 내에있는 동안 생성자를 호출 하도록 foo11정수 11을 직접 전달했습니다 . (2)와 (3)과는 달리, 우리는 이것을하기 위해 어떤 사전 종료 객체 도 필요하지 않았습니다 . 중요하게도 생성자 에 대한 호출이 하나만 발생했습니다 (생성됨 ).emplace(11, d)unordered_mapFoo(int)emplace()fooFoofoo11

  5. 그런 다음 정수 12를에 직접 전달했습니다 insert({12, d}). with emplace(11, d)( Foo생성자 가 한 번만 호출 된 결과) 와 달리이 insert({12, d})호출은 Foo의 생성자 ( foo12및 생성 foo13) 를 두 번 호출했습니다 .

사이의 주요 "큰 그림"차이가 무엇인지이 공연 insert()하고 emplace()있습니다 :

를 사용 하려면 insert() 거의 항상 범위 내 Foo에서 일부 객체 가 생성되거나 존재해야 하지만 main()(복사 또는 이동이 뒤 따르는 경우) 생성자 emplace()를 사용하면 Foo내부적으로 unordered_map(즉, emplace()메소드 정의 범위 내) 내부적으로 호출 이 수행됩니다 . 전달한 키의 인수 emplace()는 의 정의 Foo내 에서 생성자 호출 로 직접 전달됩니다 unordered_map::emplace()(선택적 추가 세부 사항 :이 새로 생성 된 객체가 즉시 unordered_map멤버 변수 중 하나로 통합되어 소멸자가 호출되지 않을 때) 실행이 중단 emplace()되고 이동 또는 복사 생성자가 호출되지 않습니다).

참고 : "그 이유 는 거의 " "에 거의 항상 아래 위의 I에 설명되어 있습니다").

  1. 계속 : 부르심 이유 umap.emplace(foo3, d)라고 Foo의 const가 아닌 복사 생성자는 다음과 같다 : 우리가 사용하고 있기 때문에 emplace(), 컴파일러가 알고있다 foo3(A const가 아닌 Foo개체) 일부에 대한 인수로 의미 Foo생성자입니다. 이 경우 가장 적합한 Foo생성자는 비 const 사본 생성자 Foo(Foo& f2)입니다. 그렇기 때문에 umap.emplace(foo3, d)복사 생성자를 호출 umap.emplace(11, d)하지 않은 이유 입니다.

발문:

I. 하나의 과부하 insert()는 실제로 와 같습니다 emplace() . 이 cppreference.com 페이지에 설명 대로 과부하 template<class P> std::pair<iterator, bool> insert(P&& value)( 이 cppreference.com 페이지의 과부하 (2)) insert()는와 같습니다 emplace(std::forward<P>(value)).

II. 여기서 어디로 가야합니까?

ㅏ. 에 대한 위의 소스 코드와 연구 문서 함께 놀러 insert()(예 : 여기 )와 emplace()(예 : 여기 )의 온라인으로 발견. eclipse 또는 NetBeans와 같은 IDE를 사용하는 경우 IDE에 과부하가 걸리 insert()거나 emplace()호출되는 것을 쉽게 알 수 있습니다 (일식에서는 마우스 커서를 잠시 동안 함수 호출 위에 안정적으로 유지하십시오). 시도해 볼 코드가 더 있습니다.

std::cout << "\numap.insert({{" << Foo::foo_counter << ", d}})\n";
umap.insert({{Foo::foo_counter, d}});
//but umap.emplace({{Foo::foo_counter, d}}); results in a compile error!

std::cout << "\numap.insert(std::pair<const Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<const Foo, int>({Foo::foo_counter, d}));
//The above uses Foo(int) and then Foo(const Foo &), as expected. but the
// below call uses Foo(int) and the move constructor Foo(Foo&&). 
//Do you see why?
std::cout << "\numap.insert(std::pair<Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<Foo, int>({Foo::foo_counter, d}));
//Not only that, but even more interesting is how the call below uses all 
// three of Foo(int) and the Foo(Foo&&) move and Foo(const Foo &) copy 
// constructors, despite the below call's only difference from the call above 
// being the additional { }.
std::cout << "\numap.insert({std::pair<Foo, int>({" << Foo::foo_counter << ", d})})\n";
umap.insert({std::pair<Foo, int>({Foo::foo_counter, d})});


//Pay close attention to the subtle difference in the effects of the next 
// two calls.
int cur_foo_counter = Foo::foo_counter;
std::cout << "\numap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}}) where " 
  << "cur_foo_counter = " << cur_foo_counter << "\n";
umap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}});

std::cout << "\numap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}}) where "
  << "Foo::foo_counter = " << Foo::foo_counter << "\n";
umap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}});


//umap.insert(std::initializer_list<std::pair<Foo, int>>({{Foo::foo_counter, d}}));
//The call below works fine, but the commented out line above gives a 
// compiler error. It's instructive to find out why. The two calls
// differ by a "const".
std::cout << "\numap.insert(std::initializer_list<std::pair<const Foo, int>>({{" << Foo::foo_counter << ", d}}))\n";
umap.insert(std::initializer_list<std::pair<const Foo, int>>({{Foo::foo_counter, d}}));

머지 않아 std::pair생성자 과부하 ( reference 참조 )가 unordered_map얼마나 많은 객체가 복사, 이동, 생성 및 / 또는 파괴되는지뿐만 아니라이 모든 상황이 발생하는 시점에 중요한 영향을 미칠 수 있음을 곧 알게 될 것입니다 .

비. 대신 다른 컨테이너 클래스 (예 : std::set또는 std::unordered_multiset) 를 사용하면 어떻게되는지 확인하십시오 std::unordered_map.

씨. 이제 범위 유형으로 대신 (대신 Goo이름이 바뀐) 객체를 사용하고 (즉, 대신 사용 ) 생성자가 몇 개이고 어떤 생성자가 있는지 확인하십시오 . (스포일러 : 효과는 있지만 극적이지는 않습니다.)Foointunordered_mapunordered_map<Foo, Goo>unordered_map<Foo, int>Goo


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