Semua yang perlu Anda ketahui tentang std :: any

Halo, Habr! Kami mempersembahkan kepada Anda terjemahan dari artikel "Semua yang Harus Anda Ketahui Tentang std :: any from C ++ 17" oleh Bartlomiej Filipek .

gambar

Dengan bantuan std::optionalAnda dapat menyimpan satu jenis jenis. Dengan bantuan std::variantAnda dapat menyimpan beberapa jenis dalam satu objek. Dan C ++ 17 memberi kami jenis pembungkus lain - std::anyyang dapat menyimpan apa pun sementara jenis lainnya aman.

Dasar


Sebelum ini, standar C ++ tidak memberikan banyak solusi untuk masalah penyimpanan beberapa tipe dalam satu variabel. Tentu saja Anda dapat menggunakannya void*, tetapi sama sekali tidak aman.

Secara teoritis, void*Anda bisa membungkusnya dalam kelas di mana Anda dapat menyimpan jenisnya:

class MyAny
{
    void* _value;
    TypeInfo _typeInfo;
};

Seperti yang Anda lihat, kami mendapatkan formulir dasar tertentu std::any, tetapi untuk memastikan keamanan tipe MyAnykami memerlukan pemeriksaan tambahan. Itulah mengapa lebih baik menggunakan opsi dari perpustakaan standar daripada membuat keputusan sendiri.

Dan ini adalah apa itu std::anydari C ++ 17. Ini memungkinkan Anda untuk menyimpan apa pun di objek dan melaporkan kesalahan (melempar pengecualian) ketika Anda mencoba mengakses dengan menentukan jenis yang salah.

Demo kecil:

std::any a(12);

//    :
a = std::string("Hello!");
a = 16;
//   :

//    a  
std::cout << std::any_cast<int>(a) << '\n'; 

//    :
try 
{
    std::cout << std::any_cast<std::string>(a) << '\n';
}
catch(const std::bad_any_cast& e) 
{
    std::cout << e.what() << '\n';
}

//        - :
a.reset();
if (!a.has_value())
{
    std::cout << "a is empty!" << "\n";
}

//    any  :
std::map<std::string, std::any> m;
m["integer"] = 10;
m["string"] = std::string("Hello World");
m["float"] = 1.0f;

for (auto &[key, val] : m)
{
    if (val.type() == typeid(int))
        std::cout << "int: " << std::any_cast<int>(val) << "\n";
    else if (val.type() == typeid(std::string))
        std::cout << "string: " << std::any_cast<std::string>(val) << "\n";
    else if (val.type() == typeid(float))
        std::cout << "float: " << std::any_cast<float>(val) << "\n";
}

Kode ini akan menampilkan:

16
bad any_cast
a is empty!
float: 1
int: 10
string: Hello World

Contoh di atas menunjukkan beberapa hal penting:

  • std::any โ€” std::optional std::variant
  • , .has_value()
  • .reset()
  • std::decay
  • ,
  • std::any_cast, bad_any_cast, ยซTยป
  • .type(), std::type_info

Contoh di atas terlihat mengesankan - variabel tipe nyata dalam C ++! Jika Anda benar-benar menyukai JavaScript, Anda bahkan dapat membuat semua variabel tipe Anda std::anydan menggunakan C ++ sebagai JavaScript :)

Tapi mungkin ada beberapa contoh penggunaan normal?

Kapan menggunakannya?


Meskipun saya void*anggap sebagai hal yang sangat tidak aman dengan rentang penggunaan yang sangat terbatas, ini std::anysepenuhnya aman untuk jenis, sehingga memiliki beberapa cara yang baik untuk menggunakannya.

Contohnya:

  • Di perpustakaan - saat perpustakaan Anda perlu menyimpan atau mentransfer beberapa data, dan Anda tidak tahu tipe data ini
  • Saat mem-parsing file - jika Anda benar-benar tidak dapat menentukan jenis apa yang didukung
  • Olahpesan
  • Interaksi Bahasa Scripting
  • Membuat penerjemah untuk bahasa skrip
  • Antarmuka Pengguna - Bidang Dapat Menyimpan Apa Pun

Tampak bagi saya bahwa dalam banyak contoh ini kita dapat menyoroti daftar terbatas dari jenis yang didukung, jadi ini std::variantmungkin pilihan yang lebih baik. Tetapi tentu saja sulit untuk membuat perpustakaan tanpa mengetahui produk akhir yang akan digunakan. Anda tidak tahu jenis apa yang akan disimpan di sana.

Peragaan menunjukkan beberapa hal dasar, tetapi di bagian berikut Anda akan belajar lebih banyak std::any, jadi teruslah membaca.

Buat std :: any


Ada beberapa cara untuk membuat objek bertipe std::any:

  • inisialisasi standar - objek kosong
  • inisialisasi langsung dengan nilai / objek
  • langsung menunjukkan jenis objek - std::in_place_type
  • melalui std::make_any

Contohnya:

//  :
std::any a;
assert(!a.has_value());

//   :
std::any a2(10); // int
std::any a3(MyType(10, 11));

// in_place:
std::any a4(std::in_place_type<MyType>, 10, 11);
std::any a5{std::in_place_type<std::string>, "Hello World"};

// make_any
std::any a6 = std::make_any<std::string>("Hello World");

Ubah nilai


Ada std::anydua cara untuk mengubah nilai yang saat ini disimpan di : metode emplaceatau tugas:

std::any a;

a = MyType(10, 11);
a = std::string("Hello");

a.emplace<float>(100.5f);
a.emplace<std::vector<int>>({10, 11, 12, 13});
a.emplace<MyType>(10, 11);

Siklus Hidup Obyek


Kunci dari keamanan std::anyadalah kurangnya kebocoran sumber daya. Untuk mencapai ini, itu akan std::anymenghancurkan objek aktif apa pun sebelum menetapkan nilai baru.

std::any var = std::make_any<MyType>();
var = 100.0f;
std::cout << std::any_cast<float>(var) << "\n";

Kode ini akan menampilkan yang berikut:

MyType::MyType
MyType::~MyType
100

Objek std::anydiinisialisasi dengan objek tipe MyType, tetapi sebelum menetapkan nilai baru (100.0f), destruktor disebut MyType.

Mendapatkan akses ke suatu nilai


Dalam kebanyakan kasus, Anda hanya memiliki satu cara untuk mengakses nilai di std::any- std::any_cast, ini mengembalikan nilai dari tipe yang ditentukan jika disimpan dalam objek.

Fitur ini sangat berguna, karena memiliki banyak cara untuk menggunakannya:

  • mengembalikan salinan nilai dan berhenti std::bad_any_castkarena kesalahan
  • mengembalikan tautan ke nilai dan berhenti std::bad_any_cast karena kesalahan
  • mengembalikan pointer ke nilai (konstan atau tidak) atau nullptr jika terjadi kesalahan

Lihat sebuah contoh:

struct MyType
{
    int a, b;

    MyType(int x, int y) : a(x), b(y) { }

    void Print() { std::cout << a << ", " << b << "\n"; }
};

int main()
{
    std::any var = std::make_any<MyType>(10, 10);
    try
    {
        std::any_cast<MyType&>(var).Print();
        std::any_cast<MyType&>(var).a = 11; // /
        std::any_cast<MyType&>(var).Print();
        std::any_cast<int>(var); // throw!
    }
    catch(const std::bad_any_cast& e) 
    {
        std::cout << e.what() << '\n';
    }

    int* p = std::any_cast<int>(&var);
    std::cout << (p ? "contains int... \n" : "doesn't contain an int...\n");

    MyType* pt = std::any_cast<MyType>(&var);
    if (pt)
    {
        pt->a = 12;
        std::any_cast<MyType&>(var).Print();
    }
}

Seperti yang Anda lihat, kami memiliki dua cara untuk melacak kesalahan: melalui pengecualian ( std::bad_any_cast) atau mengembalikan pointer (atau nullptr). Fungsi std::any_castuntuk pointer kembali kelebihan beban dan ditandai sebagai noexcept.

Performa dan penggunaan memori


std::anyIni terlihat seperti alat yang ampuh, dan Anda kemungkinan besar akan menggunakannya untuk menyimpan data dari berbagai jenis, tetapi berapa harganya?

Masalah utama adalah alokasi memori tambahan.

std::variant dan std::optionaltidak memerlukan alokasi memori tambahan, tetapi ini karena jenis data yang disimpan dalam objek diketahui sebelumnya. std :: any tidak memiliki informasi seperti itu, sehingga dapat menggunakan memori tambahan.

Apakah ini akan selalu terjadi atau kadang-kadang? Aturan mana? Apakah ini akan terjadi dengan tipe sederhana seperti int?

Mari kita lihat apa yang dikatakan standar:
Implementasi harus menghindari penggunaan memori yang dialokasikan secara dinamis untuk nilai yang terkandung kecil. Contoh: di mana objek yang dibangun hanya memegang int. Optimalisasi objek kecil seperti itu hanya akan diterapkan pada tipe T yang is_nothrow_move_constructible_v benar
Implementasi harus menghindari penggunaan memori dinamis untuk data tersimpan berukuran kecil. Misalnya, ketika sebuah objek dibuat hanya menyimpan int. Optimalisasi seperti itu untuk objek kecil seharusnya hanya diterapkan pada tipe T yang is_nothrow_move_constructible_v benar.

Akibatnya, mereka mengusulkan untuk menggunakan Small Buffer Optimization / SBO untuk implementasi. Tapi ini juga ada harganya. Ini membuat tipe lebih besar - untuk menutupi buffer.

Mari kita lihat ukurannya std::any, berikut ini adalah hasil dari beberapa kompiler:

Penyusunsizeof (apa saja)
GCC 8.1 (Coliru)enambelas
Dentang 7.0.0 (Kotak Suara)32
MSVC 2017 15.7.0 32-bit40
MSVC 2017 15.7.0 64-bit64

Secara umum, seperti yang Anda lihat, std::anyini bukan tipe yang sederhana, dan itu membawa biaya tambahan. Biasanya memakan banyak memori, karena SBO, dari 16 hingga 32 byte (dalam GCC atau dentang ... atau bahkan 64 byte dalam MSVC!).

Bermigrasi dari boost :: any


boost::anyItu diperkenalkan di suatu tempat pada tahun 2001 (versi 1.23.0). Selain itu, penulis boost::any(Kevlin Henney) juga merupakan penulis proposal std::any. Oleh karena itu, kedua jenis ini terkait erat, versi dari STL sangat didasarkan pada pendahulunya.

Berikut adalah perubahan utamanya:

FungsiBoost.Any
(1.67.0)
std :: any
Alokasi memori tambahanIyaIya
Optimasi Objek KeciltidakIya
menempatkantidakIya
in_place_type_t di konstruktortidakIya


Perbedaan utama adalah bahwa ia boost::anytidak menggunakan SBO, sehingga membutuhkan memori yang jauh lebih sedikit (dalam GCC8.1 ukurannya adalah 8 byte), tetapi karena ini, ia secara dinamis mengalokasikan memori bahkan untuk tipe kecil seperti int.

Contoh penggunaan std :: any


Kelebihan utama std::anyadalah fleksibilitas. Dalam contoh di bawah ini, Anda dapat melihat beberapa ide (atau implementasi spesifik) di mana menggunakannya std::anymembuat aplikasi sedikit lebih mudah.

Penguraian file


Dalam contoh-contoh untuk std::variant ( Anda dapat melihatnya di sini [eng] ), Anda bisa melihat bagaimana Anda dapat mengurai file konfigurasi dan menyimpan hasilnya dalam tipe variabel std::variant. Sekarang Anda menulis solusi yang sangat umum, mungkin itu adalah bagian dari beberapa perpustakaan, maka Anda mungkin tidak mengetahui semua jenis tipe yang mungkin.

Menyimpan data menggunakan std::anyparameter cenderung cukup baik dalam hal kinerja, dan pada saat yang sama memberi Anda fleksibilitas dari solusi.

Olahpesan


Di Windows Api, yang terutama ditulis dalam C, ada sistem pesan yang menggunakan id pesan dengan dua parameter opsional yang menyimpan data pesan. Berdasarkan mekanisme ini, Anda dapat mengimplementasikan WndProc, yang memproses pesan yang dikirim ke jendela Anda.

LRESULT CALLBACK WindowProc(
  _In_ HWND   hwnd,
  _In_ UINT   uMsg,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

Faktanya adalah bahwa data disimpan dalam wParamatau lParamdalam berbagai bentuk. Terkadang Anda hanya perlu menggunakan beberapa byte wParam.

Bagaimana jika kita mengubah sistem ini sehingga pesan dapat meneruskan apa pun ke metode pemrosesan?

Contohnya:

class Message
{
public:
    enum class Type 
    {
        Init,
        Closing,
        ShowWindow,        
        DrawWindow
    };

public:
    explicit Message(Type type, std::any param) :
        mType(type),
        mParam(param)
    {   }
    explicit Message(Type type) :
        mType(type)
    {   }

    Type mType;
    std::any mParam;
};

class Window
{
public:
    virtual void HandleMessage(const Message& msg) = 0;
};

Misalnya, Anda dapat mengirim pesan ke jendela:

Message m(Message::Type::ShowWindow, std::make_pair(10, 11));
yourWindow.HandleMessage(m);

Sebuah jendela dapat membalas pesan seperti ini:

switch (msg.mType) {
// ...
case Message::Type::ShowWindow:
    {
    auto pos = std::any_cast<std::pair<int, int>>(msg.mParam);
    std::cout << "ShowWidow: "
              << pos.first << ", " 
              << pos.second << "\n";
    break;
    }
}

Tentu saja, Anda harus menentukan bagaimana tipe data disimpan dalam pesan, tetapi sekarang Anda dapat menggunakan tipe nyata daripada berbagai trik dengan angka.

Properti


Dokumen asli yang diwakili oleh apa pun untuk C ++ (N1939) menunjukkan contoh objek properti:

struct property
{
    property();
    property(const std::string &, const std::any &);

    std::string name;
    std::any value;
};

typedef std::vector<property> properties;

Objek ini terlihat sangat berguna karena dapat menyimpan berbagai jenis. Yang pertama muncul di benak saya adalah contoh menggunakannya di antarmuka pengguna atau editor game.

Kami melewati perbatasan


Di r / cpp ada aliran tentang std :: any. Dan setidaknya ada satu komentar hebat yang merangkum kapan suatu tipe harus digunakan.

Dari komentar ini :
Intinya adalah std :: any memungkinkan Anda untuk mentransfer hak atas data sewenang-wenang lintas batas yang tidak tahu jenisnya.
Semua yang saya bicarakan sebelumnya dekat dengan ide ini:

  • di perpustakaan untuk antarmuka: Anda tidak tahu jenis apa yang ingin digunakan klien di sana
  • olahpesan: ide yang sama - memberikan fleksibilitas pelanggan
  • parsing file: untuk mendukung semua jenis

Total


Dalam artikel ini, kami belajar banyak tentang std::any!

Berikut adalah beberapa hal yang perlu diingat:

  • std::any bukan kelas templat
  • std::any menggunakan pengoptimalan objek kecil, sehingga tidak akan secara dinamis mengalokasikan memori untuk tipe sederhana seperti int atau double, dan untuk tipe yang lebih besar, memori tambahan akan digunakan
  • std::any bisa disebut "berat", tetapi menawarkan keamanan dan fleksibilitas yang lebih besar
  • Akses ke data std::anydapat diperoleh dengan bantuan any_cast, yang menawarkan beberapa "mode". Misalnya, jika terjadi kesalahan, ini dapat menimbulkan pengecualian atau hanya mengembalikan nullptr
  • menggunakannya ketika Anda tidak tahu persis tipe data apa yang mungkin, jika tidak pertimbangkan untuk menggunakannya std::variant

All Articles