ในแผนที่ STL ควรใช้แผนที่ :: insert มากกว่า [] หรือไม่


201

ขณะที่ผ่านมาผมได้มีการพูดคุยกับเพื่อนร่วมงานเกี่ยวกับวิธีการใส่ค่าใน STL เป็นแผนที่ ฉันชอบ map[key] = value; เพราะรู้สึกเป็นธรรมชาติและอ่านง่ายในขณะที่เขาชอบ map.insert(std::make_pair(key, value))

ฉันแค่ถามเขาและพวกเราทั้งสองไม่สามารถจำเหตุผลที่แทรกได้ดีกว่า แต่ฉันแน่ใจว่ามันไม่ใช่แค่การตั้งค่ารูปแบบ แต่มีเหตุผลทางเทคนิคเช่นประสิทธิภาพ อ้างอิง SGI STLบอกว่า "พูดอย่างเคร่งครัดฟังก์ชั่นสมาชิกนี้ไม่จำเป็น: มันมีอยู่เพื่อความสะดวกเท่านั้น"

ใครสามารถบอกเหตุผลนั้นกับฉันหรือฉันแค่ฝันว่ามี?


2
ขอบคุณสำหรับคำตอบที่ยอดเยี่ยมทั้งหมด - พวกเขามีประโยชน์จริง ๆ นี่เป็นตัวอย่างที่ดีของการโอเวอร์โฟลว์ที่ดีที่สุด ฉันถูกฉีกขาดซึ่งควรเป็นคำตอบที่ยอมรับได้: netjeff มีความชัดเจนมากขึ้นเกี่ยวกับพฤติกรรมที่แตกต่าง Greg Gregers กล่าวถึงปัญหาด้านประสิทธิภาพ หวังว่าฉันจะติ๊กทั้งคู่
danio

6
ที่จริงแล้วด้วย C ++ 11 คุณน่าจะดีที่สุดในการใช้แผนที่ :: emplaceซึ่งหลีกเลี่ยงการสร้างสองครั้ง
einpoklum

@ einpoklum: อันที่จริง Scott Meyers แนะนำเป็นอย่างอื่นในการพูดคุยของเขา "การค้นหาการพัฒนา C ++ ที่มีประสิทธิภาพ"
Thomas Eding

3
@einpoklum: นั่นเป็นกรณีเมื่อใส่เข้าไปในหน่วยความจำที่สร้างขึ้นใหม่ แต่เนื่องจากข้อกำหนดมาตรฐานบางอย่างสำหรับแผนที่จึงมีเหตุผลทางเทคนิคว่าทำไม emplace สามารถช้ากว่าการแทรกได้ สามารถใช้การพูดคุยกับ YouTube ได้อย่างอิสระเช่นลิงก์นี้youtube.com/watch?v=smqT9Io_bKo @ เครื่องหมาย 38 ~ นาที สำหรับลิงก์ SO นี่คือstackoverflow.com/questions/26446352/…
Thomas Eding

1
จริง ๆ แล้วฉันจะเถียงกับสิ่งที่เมเยอร์สนำเสนอบางอย่าง แต่นั่นอยู่นอกเหนือขอบเขตของหัวข้อความคิดเห็นนี้และต่อไปฉันคิดว่าฉันต้องถอนความคิดเห็นก่อนหน้าของฉัน
einpoklum

คำตอบ:


240

เมื่อคุณเขียน

map[key] = value;

มีวิธีที่จะบอกได้ว่าคุณไม่แทนที่valueสำหรับkeyหรือถ้าคุณสร้างขึ้นใหม่ด้วยkeyvalue

map::insert() จะสร้างเท่านั้น:

using std::cout; using std::endl;
typedef std::map<int, std::string> MyMap;
MyMap map;
// ...
std::pair<MyMap::iterator, bool> res = map.insert(MyMap::value_type(key,value));
if ( ! res.second ) {
    cout << "key " <<  key << " already exists "
         << " with value " << (res.first)->second << endl;
} else {
    cout << "created key " << key << " with value " << value << endl;
}

map[key] = valueสำหรับส่วนมากของปพลิเคชันของฉันฉันมักจะไม่สนใจว่าฉันกำลังสร้างหรือเปลี่ยนดังนั้นฉันจะใช้ให้อ่านง่ายขึ้น


16
ควรสังเกตว่าแผนที่ :: แทรกไม่เคยแทนที่ค่า และในกรณีทั่วไปฉันจะบอกว่ามันเป็นการดีกว่าที่จะใช้(res.first)->secondแทนที่จะvalueเป็นกรณีที่สอง
dalle

1
ฉันได้รับการปรับปรุงให้ชัดเจนยิ่งขึ้นว่า map :: insert ไม่เคยถูกแทนที่ ฉันออกไปelseเพราะฉันคิดว่าการใช้valueนั้นชัดเจนกว่าตัววนซ้ำ เฉพาะในกรณีที่ประเภทของค่ามีสำเนา ctor ที่ผิดปกติหรือ op == มันจะแตกต่างกันและประเภทนั้นจะทำให้เกิดปัญหาอื่น ๆ โดยใช้ภาชนะ STL เช่นแผนที่
netjeff

1
map.insert(std::make_pair(key,value))map.insert(MyMap::value_type(key,value))ควรจะเป็น ประเภทที่ส่งคืนจากmake_pairไม่ตรงกับประเภทที่ถ่ายinsertและโซลูชันปัจจุบันต้องการการแปลง
David Rodríguez - dribeas

1
มีวิธีที่จะบอกได้ว่าคุณใส่หรือมอบหมายoperator[]เพียงแค่เปรียบเทียบขนาดก่อนและหลัง Imho ความสามารถในการโทรmap::operator[]สำหรับประเภทที่สร้างได้เริ่มต้นเท่านั้นที่สำคัญกว่า
idclev 463035818

@ DavidRodríguez-dribeas: คำแนะนำที่ดีฉันอัพเดตโค้ดในคำตอบของฉัน
netjeff

53

ทั้งสองมีความหมายต่างกันเมื่อพูดถึงกุญแจที่มีอยู่แล้วในแผนที่ ดังนั้นมันจึงไม่สามารถเทียบเคียงได้โดยตรง

แต่เวอร์ชันตัวดำเนินการ [] ต้องใช้ค่าเริ่มต้นในการสร้างค่าจากนั้นจึงกำหนดค่าดังนั้นถ้าค่านี้มีราคาแพงกว่าให้คัดลอกโครงสร้างแล้วจึงจะมีราคาแพงกว่า บางครั้งการสร้างเริ่มต้นไม่สมเหตุสมผลและจากนั้นก็เป็นไปไม่ได้ที่จะใช้โอเปอเรเตอร์ []


1
make_pair อาจต้องใช้ตัวสร้างสำเนา - ซึ่งจะแย่กว่ารุ่นเริ่มต้น +1 อยู่ดี

1
สิ่งสำคัญคือตามที่คุณพูดว่าพวกเขามีความหมายที่แตกต่างกัน ดังนั้นไม่ดีไปกว่ากันเพียงใช้อันที่ทำในสิ่งที่คุณต้องการ
jalf

ทำไมตัวสร้างสำเนาจะเลวร้ายยิ่งกว่าตัวสร้างเริ่มต้นตามด้วยการกำหนด ถ้าเป็นเช่นนั้นบุคคลที่เขียนชั้นเรียนจะพลาดบางสิ่งบางอย่างเพราะสิ่งที่ตัวดำเนินการ = ทำพวกเขาควรทำแบบเดียวกันในตัวสร้างสำเนา
Steve Jessop

1
บางครั้งการสร้างเริ่มต้นมีราคาแพงเท่ากับการกำหนดเอง การก่อสร้างที่ได้รับมอบหมายและคัดลอกตามธรรมชาติจะเทียบเท่า
Greg Rogers

@Arkadiy ในบิลด์ที่ปรับให้เหมาะสมคอมไพเลอร์มักจะลบการเรียกตัวสร้างสำเนาที่ไม่จำเป็นจำนวนมาก
antred

35

อีกสิ่งที่ควรทราบด้วย std::map :

myMap[nonExistingKey]; จะสร้างรายการใหม่ในแผนที่คีย์ nonExistingKeyเริ่มต้นเป็นค่าเริ่มต้น

นี่ทำให้ฉันกลัวนรกในครั้งแรกที่ฉันเห็นมัน (ในขณะที่ต่อสู้กับข้อผิดพลาดที่น่ารังเกียจ) คงไม่ได้คาดหวัง สำหรับฉันแล้วดูเหมือนว่าจะเป็นการผ่าตัด แต่ฉันไม่ได้คาดหวังกับ "ผลข้างเคียง" ชอบmap.find()เมื่อเดินทางจากแผนที่ของคุณ


3
นั่นคือมุมมองที่ดีแม้ว่าแผนที่แฮชจะค่อนข้างเป็นสากลสำหรับรูปแบบนี้ อาจเป็นหนึ่งใน "ความแปลกประหลาดที่ไม่มีใครคิดว่าแปลก" เพียงเพราะวิธีการที่พวกเขาใช้การประชุมเดียวกันอย่างแพร่หลาย
Stephen J

19

หากการตีค่าประสิทธิภาพของคอนสตรัคเตอร์เริ่มต้นไม่ใช่ปัญหาโปรดเพื่อความรักของพระเจ้าโปรดใช้เวอร์ชันที่อ่านได้ง่ายขึ้น

:)


5
ประการที่สอง! ต้องทำเครื่องหมายนี้ ผู้คนจำนวนมากเกินไปแลกเปลี่ยนความโง่เขลาสำหรับการเร่งความเร็วระดับนาโนวินาที มีความเมตตาต่อเราวิญญาณที่ยากจนที่ต้องรักษาความโหดร้ายเช่นนี้!
Mr.Ree

6
ตามที่ Greg Rogers เขียนว่า: "ทั้งสองมีความหมายต่างกันเมื่อพูดถึงกุญแจที่มีอยู่แล้วในแผนที่ดังนั้นพวกเขาจึงไม่สามารถเทียบเคียงได้โดยตรง"
dalle

นี่เป็นคำถามและคำตอบเก่า ๆ แต่ "รุ่นที่อ่านได้ง่ายขึ้น" เป็นเหตุผลที่งี่เง่า เพราะสิ่งที่อ่านได้มากที่สุดขึ้นอยู่กับบุคคลนั้น
vallentin

14

insert ดีกว่าจากจุดยกเว้นด้านความปลอดภัย

การแสดงออกmap[key] = valueเป็นสองการดำเนินงานจริง:

  1. map[key] - การสร้างองค์ประกอบแผนที่ที่มีค่าเริ่มต้น
  2. = value - คัดลอกค่าลงในองค์ประกอบนั้น

อาจมีข้อยกเว้นเกิดขึ้นในขั้นตอนที่สอง เป็นผลให้การดำเนินการจะทำเพียงบางส่วนเท่านั้น (องค์ประกอบใหม่ถูกเพิ่มเข้าไปในแผนที่ แต่องค์ประกอบนั้นไม่ได้เริ่มต้นด้วยvalue) สถานการณ์เมื่อการดำเนินการไม่เสร็จสมบูรณ์ แต่สถานะของระบบได้รับการแก้ไขเรียกว่าการดำเนินการด้วย "ผลข้างเคียง"

insertการดำเนินการให้การรับประกันที่แข็งแกร่งหมายความว่าไม่มีผลข้างเคียง ( https://en.wikipedia.org/wiki/Exception_safety ) insertเสร็จสิ้นสมบูรณ์หรือออกจากแผนที่ในสถานะที่ไม่ได้แก้ไข

http://www.cplusplus.com/reference/map/map/insert/ :

หากจะใส่องค์ประกอบเดียวจะไม่มีการเปลี่ยนแปลงในคอนเทนเนอร์ในกรณียกเว้น (รับประกันอย่างแน่นหนา)


1
สำคัญกว่าการแทรกไม่จำเป็นต้องมีค่าที่จะเริ่มต้นที่สามารถสร้างได้
UmNyobe

13

หากใบสมัครของคุณมีความสำคัญอย่างมากฉันจะแนะนำให้ใช้ตัวดำเนินการ [] เพราะมันจะสร้างสำเนาต้นฉบับทั้งหมด 3 ฉบับโดยที่ 2 นั้นเป็นวัตถุชั่วคราวและไม่ช้าก็เร็วจะถูกทำลายเป็น

แต่ในส่วนแทรก () จะมีการสร้างวัตถุดั้งเดิมจำนวน 4 ชุดโดยที่ 3 ชิ้นเป็นวัตถุชั่วคราว (ไม่จำเป็นต้องเป็น "ชั่วคราว") และถูกทำลาย

ซึ่งหมายถึงเวลาพิเศษสำหรับ: 1. การจัดสรรหน่วยความจำหนึ่งวัตถุ 2. การเรียกตัวสร้างพิเศษหนึ่งครั้ง 3. การโทร destructor พิเศษหนึ่งครั้ง 4. การจัดสรรคืนหน่วยความจำวัตถุหนึ่งชิ้น

หากวัตถุของคุณมีขนาดใหญ่คอนสตรัคเตอร์ก็เป็นแบบปกติ destructors จะช่วยประหยัดทรัพยากรได้มาก เกี่ยวกับความสามารถในการอ่านฉันคิดว่าทั้งคู่มีความยุติธรรมเพียงพอ

คำถามเดียวกันเข้ามาในใจของฉัน แต่ไม่เกินอ่านง่าย แต่ความเร็ว นี่คือตัวอย่างโค้ดที่ฉันรู้เกี่ยวกับประเด็นที่ฉันพูดถึง

class Sample
{
    static int _noOfObjects;

    int _objectNo;
public:
    Sample() :
        _objectNo( _noOfObjects++ )
    {
        std::cout<<"Inside default constructor of object "<<_objectNo<<std::endl;
    }

    Sample( const Sample& sample) :
    _objectNo( _noOfObjects++ )
    {
        std::cout<<"Inside copy constructor of object "<<_objectNo<<std::endl;
    }

    ~Sample()
    {
        std::cout<<"Destroying object "<<_objectNo<<std::endl;
    }
};
int Sample::_noOfObjects = 0;


int main(int argc, char* argv[])
{
    Sample sample;
    std::map<int,Sample> map;

    map.insert( std::make_pair<int,Sample>( 1, sample) );
    //map[1] = sample;
    return 0;
}

เอาต์พุตเมื่อใช้การแทรก () เอาต์พุตเมื่อใช้โอเปอเรเตอร์ []


4
ตอนนี้เรียกใช้การทดสอบนั้นอีกครั้งโดยเปิดใช้งานการปรับให้เหมาะสมเต็มรูปแบบ
antred

2
นอกจากนี้ให้พิจารณาว่าตัวดำเนินการ [] ทำอะไรได้จริง ก่อนอื่นจะค้นหาแผนที่สำหรับรายการที่ตรงกับคีย์ที่ระบุ หากพบหนึ่งก็จะเขียนทับค่าของรายการที่มีการระบุไว้ หากไม่เป็นเช่นนั้นจะเป็นการแทรกรายการใหม่พร้อมคีย์และค่าที่ระบุ ยิ่งแผนที่ของคุณใหญ่ขึ้นเท่าไหร่มันจะใช้เวลาโอเปอเรเตอร์ [] ในการค้นหาแผนที่นานขึ้น ในบางจุดสิ่งนี้จะทำขึ้นสำหรับการคัดลอก c'tor พิเศษเพิ่มเติม (หากยังคงอยู่ในโปรแกรมสุดท้ายหลังจากคอมไพเลอร์ได้ทำการปรับให้เหมาะสมแล้ว)
antred

1
@antred insertต้องทำการค้นหาเดียวกันดังนั้นจึงไม่แตกต่างจาก[](เนื่องจากคีย์แผนที่นั้นไม่ซ้ำกัน)
Sz.

ภาพประกอบที่ดีเกี่ยวกับสิ่งที่เกิดขึ้นกับงานพิมพ์ - แต่โค้ด 2 บิตเหล่านี้กำลังทำอะไรอยู่? เหตุใดจึงจำเป็นต้องมีสำเนาต้นฉบับ 3 ชิ้น
rbennett485

1. ต้องใช้ตัวดำเนินการที่ได้รับมอบหมายหรือคุณจะได้รับตัวเลขที่ไม่ถูกต้องใน destructor 2. ใช้ std :: move เมื่อสร้างคู่เพื่อหลีกเลี่ยงการสร้างสำเนาที่เกินกว่า "map.insert (std :: make_pair <int, Sample> (1, std: : move (ตัวอย่าง))); "
Fl0

10

ตอนนี้ใน c ++ 11 ฉันคิดว่าวิธีที่ดีที่สุดในการแทรกคู่ในแผนที่ STL คือ:

typedef std::map<int, std::string> MyMap;
MyMap map;

auto& result = map.emplace(3,"Hello");

ผลจะเป็นคู่กับ:

  • องค์ประกอบแรก (result.first) ชี้ไปที่คู่ที่ถูกแทรกหรือชี้ไปที่คู่ด้วยปุ่มนี้ถ้าคีย์มีอยู่แล้ว

  • องค์ประกอบที่สอง (result.second) จริงถ้าการแทรกนั้นถูกต้องหรือเท็จสิ่งที่ผิดพลาด

PS: ถ้าคุณไม่กรณีเกี่ยวกับการสั่งซื้อคุณสามารถใช้ std :: unordered_map;)

ขอบคุณ!


9

gotcha พร้อมแผนที่ :: insert () คือมันจะไม่แทนที่ค่าหากคีย์มีอยู่แล้วในแผนที่ ฉันเห็นรหัส C ++ ที่เขียนโดยโปรแกรมเมอร์ Java ที่พวกเขาคาดว่าจะแทรก () เพื่อทำงานในลักษณะเดียวกับ Map.put () ใน Java ที่มีการแทนที่ค่า


2

One note คือคุณสามารถใช้Boost.Assign :

using namespace std;
using namespace boost::assign; // bring 'map_list_of()' into scope

void something()
{
    map<int,int> my_map = map_list_of(1,2)(2,3)(3,4)(4,5)(5,6);
}

1

นี่คืออีกตัวอย่างหนึ่งซึ่งแสดงว่าoperator[] เขียนทับค่าสำหรับคีย์ถ้ามีอยู่ แต่.insert จะไม่เขียนทับค่าหากมีอยู่

void mapTest()
{
  map<int,float> m;


  for( int i = 0 ; i  <=  2 ; i++ )
  {
    pair<map<int,float>::iterator,bool> result = m.insert( make_pair( 5, (float)i ) ) ;

    if( result.second )
      printf( "%d=>value %f successfully inserted as brand new value\n", result.first->first, result.first->second ) ;
    else
      printf( "! The map already contained %d=>value %f, nothing changed\n", result.first->first, result.first->second ) ;
  }

  puts( "All map values:" ) ;
  for( map<int,float>::iterator iter = m.begin() ; iter !=m.end() ; ++iter )
    printf( "%d=>%f\n", iter->first, iter->second ) ;

  /// now watch this.. 
  m[5]=900.f ; //using operator[] OVERWRITES map values
  puts( "All map values:" ) ;
  for( map<int,float>::iterator iter = m.begin() ; iter !=m.end() ; ++iter )
    printf( "%d=>%f\n", iter->first, iter->second ) ;

}

1

นี่เป็นกรณีที่ค่อนข้าง จำกัด แต่การตัดสินจากความคิดเห็นที่ฉันได้รับฉันคิดว่ามันควรค่าแก่การสังเกต

ฉันเคยเห็นผู้คนในอดีตใช้แผนที่ในรูปแบบของ

map< const key, const val> Map;

เพื่อหลีกเลี่ยงกรณีของการเขียนทับค่าโดยไม่ตั้งใจ แต่จากนั้นไปข้างหน้าเขียนในรหัสบิตอื่น ๆ :

const_cast< T >Map[]=val;

เหตุผลของพวกเขาในการทำสิ่งนี้ขณะที่ฉันจำได้ก็เพราะพวกเขาแน่ใจว่าในส่วนของรหัสบางส่วนพวกเขาจะไม่ถูกเขียนทับค่าแผนที่ ดังนั้นไปข้างหน้าด้วยวิธีการ 'อ่านได้' []มากขึ้น

จริง ๆ แล้วฉันไม่เคยมีปัญหาโดยตรงจากรหัสที่คนเหล่านี้เขียน แต่ฉันรู้สึกถึงความเสี่ยงอย่างมากจนถึงทุกวันนี้ที่มีความเสี่ยงเล็ก ๆ น้อย ๆ ที่ควรหลีกเลี่ยงเมื่อพวกเขาสามารถหลีกเลี่ยงได้ง่าย

ในกรณีที่คุณจัดการกับค่าแผนที่ที่อย่างจะต้องไม่insertถูกเขียนทับการใช้งาน อย่าสร้างข้อยกเว้นเพียงเพื่อให้สามารถอ่านได้


หลายคนต่างเขียนว่า ใช้อย่างแน่นอนinsert (ไม่ใช่input) เนื่องจากconst_castจะทำให้ค่าก่อนหน้านี้ถูกเขียนทับซึ่งไม่ใช่ค่าคงที่มาก constหรือไม่ได้ทำเครื่องหมายประเภทค่าเป็น (สิ่งของแบบนั้นมักเป็นผลลัพธ์ที่ดีที่สุดconst_castดังนั้นจึงเกือบจะเป็นธงสีแดงที่บ่งบอกถึงข้อผิดพลาดที่อื่น)
Potatoswatter

@Potatoswatter คุณพูดถูก ฉันแค่เห็นว่า const_cast [] ใช้กับค่าแผนที่ const โดยบางคนเมื่อพวกเขาแน่ใจว่าพวกเขาจะไม่เปลี่ยนค่าเก่าในรหัสบิตบางอย่าง เนื่องจาก [] ตัวเองสามารถอ่านได้มากขึ้น ดังที่ฉันได้กล่าวถึงในคำตอบสุดท้ายของฉันฉันขอแนะนำให้ใช้insertในกรณีที่คุณต้องการป้องกันค่าจากการถูกเขียนทับ (เพียงแค่เปลี่ยนinputไปinsert- ขอบคุณ)
dk123

@Potatoswatter ถ้าฉันจำได้ถูกต้องสาเหตุหลักที่คนใช้const_cast<T>(map[key])คือ 1. [] อ่านได้ง่ายขึ้น 2. พวกเขามั่นใจในโค้ดบางบิตพวกเขาจะไม่เขียนทับค่าและ 3 พวกเขาไม่ได้ ต้องการบิตอื่น ๆ ของไม่รู้รหัสเขียนทับค่าของพวกเขา - const valueด้วยเหตุนี้
dk123

2
ฉันไม่เคยได้ยินเรื่องนี้มาก่อน คุณเห็นสิ่งนี้ที่ไหน การเขียนconst_castดูเหมือนจะเป็นการลบล้าง "ความสามารถในการอ่าน" ที่พิเศษ[]และความมั่นใจแบบนั้นเกือบจะเพียงพอแล้วที่จะทำให้นักพัฒนาซอฟต์แวร์ เงื่อนไขรันไทม์หากินได้รับการแก้ไขโดยการออกแบบที่กันกระสุนไม่ได้มีความรู้สึกทางเดินอาหาร
Potatoswatter

@Potatoswatter ฉันจำได้ว่ามันเป็นหนึ่งในงานที่ผ่านมาของฉันในการพัฒนาเกมการศึกษา ฉันไม่เคยทำให้คนเขียนรหัสเพื่อเปลี่ยนนิสัยของพวกเขา คุณพูดถูกและฉันเห็นด้วยอย่างยิ่งกับคุณ จากความคิดเห็นของคุณฉันได้ตัดสินใจแล้วว่านี่น่าจะคุ้มค่ามากกว่าคำตอบเดิมของฉันและด้วยเหตุนี้ฉันจึงได้อัปเดตเพื่อสะท้อนสิ่งนี้ ขอบคุณ!
dk123

1

ความจริงที่ว่าinsert()ฟังก์ชั่นstd :: map ไม่ได้เขียนทับค่าที่เชื่อมโยงกับคีย์ทำให้เราสามารถเขียนโค้ดการแจงนับวัตถุดังนี้:

string word;
map<string, size_t> dict;
while(getline(cin, word)) {
    dict.insert(make_pair(word, dict.size()));
}

มันเป็นปัญหาที่พบได้บ่อยเมื่อเราต้องการแมปออบเจกต์ที่ไม่ซ้ำกันที่แตกต่างกับ id ของบางคนในช่วง 0..N id เหล่านั้นสามารถใช้ในภายหลังตัวอย่างเช่นในอัลกอริทึมกราฟ ทางเลือกที่มีoperator[]จะดูน้อยอ่านในความคิดของฉัน:

string word;
map<string, size_t> dict;
while(getline(cin, word)) {
    size_t sz = dict.size();
    if (!dict.count(word))
        dict[word] = sz; 
} 
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.