C ++ 튜플 대 구조체


97

a std::tuple와 데이터 전용을 사용하는 데 차이가 struct있습니까?

typedef std::tuple<int, double, bool> foo_t;

struct bar_t {
    int id;
    double value;
    bool dirty;
}

온라인에서 찾은 내용에서 두 가지 주요 차이점이 있음을 발견했습니다. 즉 struct, 더 읽기 쉽고, tuple사용할 수있는 일반 함수가 많습니다. 상당한 성능 차이가 있어야합니까? 또한 데이터 레이아웃이 서로 호환됩니까 (교체 가능)?


캐스트 질문 에 대해 잊었다 고 언급했습니다 .의 구현 tuple은 구현이 정의되어 있으므로 구현에 따라 다릅니다. 개인적으로 나는 그것에 의지 하지 않을 것입니다.
Matthieu M.

답변:


33

우리는 튜플과 struct에 대해 비슷한 논의를하고 있으며, 튜플과 struct 간의 성능 측면에서 차이를 식별하기 위해 동료 중 한 사람의 도움을 받아 간단한 벤치 마크를 작성합니다. 먼저 기본 구조체와 튜플로 시작합니다.

struct StructData {
    int X;
    int Y;
    double Cost;
    std::string Label;

    bool operator==(const StructData &rhs) {
        return std::tie(X,Y,Cost, Label) == std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
    }

    bool operator<(const StructData &rhs) {
        return X < rhs.X || (X == rhs.X && (Y < rhs.Y || (Y == rhs.Y && (Cost < rhs.Cost || (Cost == rhs.Cost && Label < rhs.Label)))));
    }
};

using TupleData = std::tuple<int, int, double, std::string>;

그런 다음 Celero를 사용하여 간단한 구조체와 튜플의 성능을 비교합니다. 다음은 gcc-4.9.2 및 clang-4.0.0을 사용하여 수집 된 벤치 마크 코드 및 성능 결과입니다.

std::vector<StructData> test_struct_data(const size_t N) {
    std::vector<StructData> data(N);
    std::transform(data.begin(), data.end(), data.begin(), [N](auto item) {
        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_int_distribution<> dis(0, N);
        item.X = dis(gen);
        item.Y = dis(gen);
        item.Cost = item.X * item.Y;
        item.Label = std::to_string(item.Cost);
        return item;
    });
    return data;
}

std::vector<TupleData> test_tuple_data(const std::vector<StructData> &input) {
    std::vector<TupleData> data(input.size());
    std::transform(input.cbegin(), input.cend(), data.begin(),
                   [](auto item) { return std::tie(item.X, item.Y, item.Cost, item.Label); });
    return data;
}

constexpr int NumberOfSamples = 10;
constexpr int NumberOfIterations = 5;
constexpr size_t N = 1000000;
auto const sdata = test_struct_data(N);
auto const tdata = test_tuple_data(sdata);

CELERO_MAIN

BASELINE(Sort, struct, NumberOfSamples, NumberOfIterations) {
    std::vector<StructData> data(sdata.begin(), sdata.end());
    std::sort(data.begin(), data.end());
    // print(data);

}

BENCHMARK(Sort, tuple, NumberOfSamples, NumberOfIterations) {
    std::vector<TupleData> data(tdata.begin(), tdata.end());
    std::sort(data.begin(), data.end());
    // print(data);
}

clang-4.0.0으로 수집 된 성능 결과

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    196663.40000 |            5.08 | 
Sort            | tuple           | Null            |              10 |               5 |         0.92471 |    181857.20000 |            5.50 | 
Complete.

그리고 gcc-4.9.2를 사용하여 수집 된 성능 결과

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    219096.00000 |            4.56 | 
Sort            | tuple           | Null            |              10 |               5 |         0.91463 |    200391.80000 |            4.99 | 
Complete.

위의 결과에서 우리는

  • 튜플은 기본 구조체보다 빠릅니다.

  • clang의 바이너리 생성은 gcc의 성능보다 더 높습니다. clang-vs-gcc는이 토론의 목적이 아니므로 자세한 내용은 다루지 않겠습니다.

우리 모두는 모든 단일 구조체 정의에 대해 == 또는 <또는> 연산자를 작성하는 것이 고통스럽고 버그가 많은 작업이라는 것을 알고 있습니다. std :: tie를 사용하여 사용자 지정 비교기를 교체하고 벤치 마크를 다시 실행합니다.

bool operator<(const StructData &rhs) {
    return std::tie(X,Y,Cost, Label) < std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
}

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    200508.20000 |            4.99 | 
Sort            | tuple           | Null            |              10 |               5 |         0.90033 |    180523.80000 |            5.54 | 
Complete.

이제 std :: tie를 사용하면 코드가 더 우아하고 실수하기 어렵지만 약 1 %의 성능이 저하된다는 것을 알 수 있습니다. 부동 소수점 숫자를 사용자 정의 된 비교기와 비교하는 것에 대한 경고도 받기 때문에 지금은 std :: tie 솔루션을 사용하겠습니다.

지금까지 구조체 코드를 더 빠르게 실행할 수있는 솔루션이 없습니다. 스왑 함수를 살펴보고 성능을 얻을 수 있는지 다시 작성해 보겠습니다.

struct StructData {
    int X;
    int Y;
    double Cost;
    std::string Label;

    bool operator==(const StructData &rhs) {
        return std::tie(X,Y,Cost, Label) == std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
    }

    void swap(StructData & other)
    {
        std::swap(X, other.X);
        std::swap(Y, other.Y);
        std::swap(Cost, other.Cost);
        std::swap(Label, other.Label);
    }  

    bool operator<(const StructData &rhs) {
        return std::tie(X,Y,Cost, Label) < std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
    }
};

clang-4.0.0을 사용하여 수집 된 성능 결과

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    176308.80000 |            5.67 | 
Sort            | tuple           | Null            |              10 |               5 |         1.02699 |    181067.60000 |            5.52 | 
Complete.

그리고 gcc-4.9.2를 사용하여 수집 된 성능 결과

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    198844.80000 |            5.03 | 
Sort            | tuple           | Null            |              10 |               5 |         1.00601 |    200039.80000 |            5.00 | 
Complete.

이제 우리 구조체는 튜플보다 약간 빠르지 만 (clang에서는 약 3 %, gcc에서는 1 % 미만), 모든 구조체에 대해 사용자 지정 스왑 함수를 작성해야합니다.


24

코드에서 여러 개의 다른 튜플을 사용하는 경우 사용중인 펑터의 수를 압축 할 수 있습니다. 다음과 같은 형태의 펑터를 자주 사용했기 때문에 이렇게 말합니다.

template<int N>
struct tuple_less{
    template<typename Tuple>
    bool operator()(const Tuple& aLeft, const Tuple& aRight) const{
        typedef typename boost::tuples::element<N, Tuple>::type value_type;
        BOOST_CONCEPT_REQUIRES((boost::LessThanComparable<value_type>));

        return boost::tuples::get<N>(aLeft) < boost::tuples::get<N>(aRight);
    }
};

이것은 과잉처럼 보일 수 있지만 구조체 내의 각 위치에 대해 구조체를 사용하여 완전히 새로운 functor 객체를 만들어야하지만 튜플의 경우 변경 N합니다. 그보다 더 좋은 점은 각 구조체와 각 멤버 변수에 대해 완전히 새로운 펑터를 만드는 것과는 반대로 모든 단일 튜플에 대해이 작업을 수행 할 수 있다는 것입니다. NxM 펑터가있는 M 멤버 변수가있는 N 구조체가있는 경우 코드 하나로 압축 할 수있는 (최악의 경우 시나리오) 생성해야합니다.

당연히 튜플 방식을 사용하려는 경우 작업을위한 Enum도 만들어야합니다.

typedef boost::tuples::tuple<double,double,double> JackPot;
enum JackPotIndex{
    MAX_POT,
    CURRENT_POT,
    MIN_POT
};

그리고 붐, 당신은 코드를 완전히 읽을 수 있습니다.

double guessWhatThisIs = boost::tuples::get<CURRENT_POT>(someJackPotTuple);

그 안에 포함 된 항목을 얻고 자 할 때 자신을 설명하기 때문입니다.


8
어 ... C ++에는 함수 포인터 template <typename C, typename T, T C::*> struct struct_less { template <typename C> bool operator()(C const&, C const&) const; };가 있으므로 가능해야합니다. 철자는 약간 덜 편리하지만 한 번만 작성됩니다.
Matthieu M.

17

튜플은 기본 (== 및! =에 대해 모든 요소를 ​​비교합니다. <. <= ...이 두 번째를 비교하면 먼저 비교합니다 ...) 비교기 : http://en.cppreference.com/w/ cpp / 유틸리티 / 튜플 / operator_cmp

편집 : 주석에서 언급했듯이 C ++ 20 우주선 연산자는 한 줄의 코드로이 기능을 지정하는 방법을 제공합니다.


1
C ++ 20에서는 우주선 연산자를 사용하여 최소한의 상용구로이 문제가 해결되었습니다 .
John McFarlane

6

음, 여기에 struct operator == () 내부에 많은 튜플을 구성하지 않는 벤치 마크가 있습니다. POD 사용으로 인한 성능 영향이 전혀 없다는 점을 고려할 때 튜플을 사용하면 성능에 상당한 영향을 미칩니다. (주소 해석기는 로직 유닛이 값을보기 전에 명령어 파이프 라인에서 값을 찾습니다.)

기본 'Release'설정을 사용하여 VS2015CE를 사용하여 내 컴퓨터에서 실행 한 일반적인 결과 :

Structs took 0.0814905 seconds.
Tuples took 0.282463 seconds.

당신이 만족할 때까지 그것으로 원숭이하십시오.

#include <iostream>
#include <string>
#include <tuple>
#include <vector>
#include <random>
#include <chrono>
#include <algorithm>

class Timer {
public:
  Timer() { reset(); }
  void reset() { start = now(); }

  double getElapsedSeconds() {
    std::chrono::duration<double> seconds = now() - start;
    return seconds.count();
  }

private:
  static std::chrono::time_point<std::chrono::high_resolution_clock> now() {
    return std::chrono::high_resolution_clock::now();
  }

  std::chrono::time_point<std::chrono::high_resolution_clock> start;

};

struct ST {
  int X;
  int Y;
  double Cost;
  std::string Label;

  bool operator==(const ST &rhs) {
    return
      (X == rhs.X) &&
      (Y == rhs.Y) &&
      (Cost == rhs.Cost) &&
      (Label == rhs.Label);
  }

  bool operator<(const ST &rhs) {
    if(X > rhs.X) { return false; }
    if(Y > rhs.Y) { return false; }
    if(Cost > rhs.Cost) { return false; }
    if(Label >= rhs.Label) { return false; }
    return true;
  }
};

using TP = std::tuple<int, int, double, std::string>;

std::pair<std::vector<ST>, std::vector<TP>> generate() {
  std::mt19937 mt(std::random_device{}());
  std::uniform_int_distribution<int> dist;

  constexpr size_t SZ = 1000000;

  std::pair<std::vector<ST>, std::vector<TP>> p;
  auto& s = p.first;
  auto& d = p.second;
  s.reserve(SZ);
  d.reserve(SZ);

  for(size_t i = 0; i < SZ; i++) {
    s.emplace_back();
    auto& sb = s.back();
    sb.X = dist(mt);
    sb.Y = dist(mt);
    sb.Cost = sb.X * sb.Y;
    sb.Label = std::to_string(sb.Cost);

    d.emplace_back(std::tie(sb.X, sb.Y, sb.Cost, sb.Label));
  }

  return p;
}

int main() {
  Timer timer;

  auto p = generate();
  auto& structs = p.first;
  auto& tuples = p.second;

  timer.reset();
  std::sort(structs.begin(), structs.end());
  double stSecs = timer.getElapsedSeconds();

  timer.reset();
  std::sort(tuples.begin(), tuples.end());
  double tpSecs = timer.getElapsedSeconds();

  std::cout << "Structs took " << stSecs << " seconds.\nTuples took " << tpSecs << " seconds.\n";

  std::cin.get();
}

감사합니다. 나는 최적화 할 때 것으로 나타났습니다 -O3, tuples보다 적은 시간이 걸렸습니다 structs.
Simog

3

글쎄, POD 구조체는 종종 (ab) 저수준 연속 청크 읽기 및 직렬화에 사용될 수 있습니다. 말했듯이 튜플은 특정 상황에서 더 최적화되고 더 많은 기능을 지원할 수 있습니다.

상황에 더 적합한 것을 사용하십시오. 일반적인 선호 사항은 없습니다. 성능 차이는 크지 않을 것이라고 생각합니다 (하지만 벤치마킹하지는 않았습니다). 데이터 레이아웃은 대부분 호환되지 않고 구현에 따라 다릅니다.


3

"일반적인 함수"에 관한 한, Boost.Fusion은 약간의 사랑을받을 자격이 있습니다 . 특히 BOOST_FUSION_ADAPT_STRUCT .

페이지에서 추출 : ABRACADBRA

namespace demo
{
    struct employee
    {
        std::string name;
        int age;
    };
}

// demo::employee is now a Fusion sequence
BOOST_FUSION_ADAPT_STRUCT(
    demo::employee
    (std::string, name)
    (int, age))

즉, 모든 Fusion 알고리즘이 이제 struct에 적용됩니다 demo::employee.


편집 : 성능 차이 또는 레이아웃 호환성과 관련하여 tuple의 레이아웃은 구현이 정의되어 호환되지 않으므로 (따라서 두 표현 사이에 캐스트해서는 안 됨) 일반적으로 성능 측면에서 (적어도 릴리스에서는) 차이가 없습니다. 인라인 get<N>.


16
나는 이것이 가장 많이 뽑힌 답변이라고 생각하지 않습니다. 질문에 대한 답변조차하지 않습니다. 문제는 부스트가 아니라 tuples와 structs 에 관한 것입니다 !
gsamaras 2014

@G. Samaras : 문제는 튜플과의 차이 struct, 특히 구조체를 조작하는 알고리즘이없는 것에 대해 튜플을 조작하는 알고리즘이 풍부 하다는 것입니다 (필드를 반복하여 시작). 이 답변은 Boost.Fusion을 사용하여 struct튜플에있는 것만 큼 많은 알고리즘을 가져옴으로써이 간격을 메울 수 있음을 보여줍니다 . 나는 정확히 두 가지 질문에 대해 작은 설명을 추가했습니다.
Matthieu M.

3

또한 데이터 레이아웃이 서로 호환됩니까 (교환 가능)?

이상하게도 질문의이 부분에 대한 직접적인 응답을 볼 수 없습니다.

대답은 아니오 입니다. 또는 튜플의 레이아웃이 지정되지 않았기 때문에 적어도 신뢰할 수 없습니다.

첫째, 구조체는 표준 레이아웃 유형 입니다. 멤버의 순서, 패딩 및 정렬은 표준 및 플랫폼 ABI의 조합에 의해 잘 정의됩니다.

튜플이 표준 레이아웃 유형이고 유형이 지정된 순서로 필드가 배치되었음을 알고 있다면 구조체와 일치 할 것이라는 확신을 가질 수 있습니다.

튜플은 일반적으로 이전 Loki / Modern C ++ 디자인 재귀 스타일 또는 최신 가변 스타일 중 하나로 상속을 사용하여 구현됩니다. 둘 다 다음 조건을 위반하므로 둘 다 표준 레이아웃 유형이 아닙니다.

  1. (C ++ 14 이전)

    • 비 정적 데이터 멤버가있는 기본 클래스가 없거나

    • 가장 많이 파생 된 클래스에 비 정적 데이터 멤버가없고 비 정적 데이터 멤버가있는 기본 클래스가 하나만 있습니다.

  2. (C ++ 14 이상)

    • 모든 비 정적 데이터 멤버와 비트 필드가 동일한 클래스에 선언되어 있습니다 (모두 파생 또는 일부 기본에 모두 있음).

각 리프 기본 클래스에는 단일 튜플 요소가 포함되어 있기 때문입니다 (주의 : 단일 요소 튜플 유용하지는 않지만 표준 레이아웃 유형일 수 있음). 따라서 표준이 튜플이 구조체와 동일한 패딩 또는 정렬을 갖도록 보장 하지 않는다는 것을 알고 있습니다.

또한 이전의 재귀 스타일 튜플은 일반적으로 데이터 멤버를 역순으로 배치한다는 점에 주목할 가치가 있습니다.

일화로, 과거에 일부 컴파일러 및 필드 유형 조합에서 실제로 작동했습니다 (한 경우에는 필드 순서를 반대로 한 후 재귀 튜플 사용). 지금은 확실히 (컴파일러, 버전 등에서) 안정적으로 작동하지 않으며 처음부터 보장되지 않았습니다.


2

내 경험은 시간이 지남에 따라 기능이 순수한 데이터 보유자였던 유형 (예 : POD 구조체)에 적용되기 시작한다는 것입니다. 데이터에 대한 내부 지식, 불변 유지 등을 요구하지 않아야하는 특정 수정과 같은 것.

그것은 좋은 일입니다. 그것은 물체 지향의 기초입니다. 클래스가있는 C가 발명 된 이유입니다. 튜플과 같은 순수한 데이터 수집을 사용하는 것은 그러한 논리적 확장에 개방되지 않습니다. 구조체는 있습니다. 그래서 거의 항상 구조체를 선택합니다.

모든 "개방형 데이터 객체"와 마찬가지로 튜플은 정보 숨김 패러다임을 위반합니다. 당신은 할 수없는 튜플 도매를 던지는없이 나중에 변경합니다. 구조체를 사용하면 점차적으로 액세스 함수로 이동할 수 있습니다.

또 다른 문제는 유형 안전성과 자체 문서화 코드입니다. 함수가 유형의 객체를 inbound_telegram받거나 location_3D명확한 경우; 수신 unsigned char *하거나 수신 tuple<double, double, double>하지 않는 경우 : 텔레 그램은 아웃 바운드 일 수 있으며 튜플은 위치 대신 번역 일 수 있거나 긴 주말의 최소 온도 판독 값일 수 있습니다. 예, 의도를 명확하게하기 위해 typedef를 사용할 수 있지만 실제로 온도를 통과하는 것을 막지는 않습니다.

이러한 문제는 특정 규모를 초과하는 프로젝트에서 중요 해지는 경향이 있습니다. 튜플의 단점과 정교한 클래스의 장점은 눈에 띄지 않게되며 실제로 소규모 프로젝트에서는 오버 헤드가됩니다. 눈에 띄지 않는 작은 데이터 집합에 대해서도 적절한 클래스로 시작하면 늦은 배당금이 지급됩니다.

물론 실행 가능한 한 가지 전략은 해당 데이터에 대한 작업을 제공하는 클래스 래퍼의 기본 데이터 공급자로 순수한 데이터 홀더를 사용하는 것입니다.


1

성능 차이가 있어서는 안됩니다 (사소한 차이라도). 최소한 일반적인 경우에는 동일한 메모리 레이아웃이 생성됩니다. 그럼에도 불구하고, 그들 사이의 캐스팅은 아마도 작동하는 데 필요하지 않을 것입니다 (일반적으로 그렇게 될 가능성이 꽤 타당하다고 생각합니다).


4
사실 약간의 차이가있을 것 같아요. A struct는 각 하위 개체에 대해 최소 1 바이트를 할당해야하지만 tuple빈 개체를 최적화 하는 데 도움이 될 수 있다고 생각 합니다. 또한 패킹 및 정렬과 관련하여 튜플에 더 많은 여유가있을 수 있습니다.
Matthieu M.

1

속도 나 레이아웃에 대해 걱정하지 마십시오. 이는 나노 최적화이며 컴파일러에 따라 다르며 결정에 영향을 미칠만큼 충분한 차이가 없습니다.

의미있게 함께 속한 것들에 구조체를 사용하여 전체를 형성합니다.

우연히 함께있는 것들에 튜플을 사용합니다. 코드에서 튜플을 자발적으로 사용할 수 있습니다.


1

다른 답변으로 판단하면 성능 고려 사항은 기껏해야 최소한입니다.

따라서 실용성, 가독성 및 유지 보수성으로 귀결되어야합니다. 그리고 struct그것을 읽고 이해하기 쉽게 유형을 만들기 때문에 더 일반적이다.

때로는 매우 일반적인 방식으로 코드를 처리하기 위해 std::tuple(또는 심지어 std::pair)가 필요할 수 있습니다. 예를 들어 가변 매개 변수 팩과 관련된 일부 작업은 std::tuple. 코드를 개선 std::tiestd::tuple수 있는 경우 (C ++ 20 이전 )에 대한 좋은 예입니다 .

그러나 어디에서든지 당신이 할 수 를 사용 struct, 당신은 아마 한다 을 사용합니다 struct. 유형의 요소에 의미 론적 의미를 부여합니다. 유형을 이해하고 사용하는 데 매우 중요합니다. 결과적으로 이것은 어리석은 실수를 피하는 데 도움이 될 수 있습니다.

// hard to get wrong; easy to understand
cat.arms = 0;
cat.legs = 4;

// easy to get wrong; hard to understand
std::get<0>(cat) = 0;
std::get<1>(cat) = 4;

0

나는 그것이 오래된 테마라는 것을 알고 있지만, 이제 내 프로젝트의 일부에 대해 결정을 내리려고합니다. 튜플 방식이나 구조체 방식으로 갈까요? 이 스레드를 읽은 후 몇 가지 아이디어가 있습니다.

  1. wheaties 및 성능 테스트에 대해 : 일반적으로 구조체에 대해 memcpy, memset 및 유사한 트릭을 사용할 수 있습니다. 이것은 튜플보다 성능을 훨씬 더 좋게 만듭니다.

  2. 튜플에서 몇 가지 장점이 있습니다.

    • 튜플을 사용하여 함수 또는 메서드에서 변수 컬렉션을 반환하고 사용하는 유형의 수를 줄일 수 있습니다.
    • 튜플에 <, ==,> 연산자가 미리 정의되어 있다는 사실에 따라 튜플을 map 또는 hash_map의 키로 사용할 수도 있습니다. 이는 이러한 연산자를 구현해야하는 구조보다 훨씬 더 비용 효율적입니다.

웹을 검색 한 결과이 페이지에 도달했습니다 : https://arne-mertz.de/2017/03/smelly-pair-tuple/

일반적으로 위의 최종 결론에 동의합니다.


1
이것은 특정 질문에 대한 답변이 아니라 작업중인 것과 더 비슷하게 들립니다.
Dieter Meemken

튜플과 함께 memcpy를 사용하는 것을 막는 것은 없습니다.
Peter-Monica 복원
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.