Oh, itu std :: make_share ...

C ++ Core Guidelines berisi aturan R22 , yang menginstruksikan untuk menggunakan std :: make_share alih-alih memanggil konstruktor std :: shared_ptr. Hanya ada satu argumen dalam Pedoman Inti untuk penghematan keputusan seperti pada alokasi (dan deallokasi).



Dan jika Anda menggali lebih dalam?



std :: make_share bermanfaat



Mengapa std :: make_ shared muncul di STL?



Ada contoh kanonik di mana membangun std :: shared_ptr dari pointer mentah yang baru dibuat dapat menyebabkan kebocoran memori:



process(std::shared_ptr<Bar>(new Bar), foo());


Untuk menghitung argumen fungsi proses (...), Anda harus memanggil:



  • Bar baru;
  • constructor std :: shared_ptr;
  • foo ().


Kompilator dapat mencampurnya dalam urutan apa pun, misalnya seperti ini:



  • Bar baru;
  • foo ();
  • constructor std :: shared_ptr.


Jika pengecualian terjadi di foo (), kami mendapatkan bocoran instance Bar.



Tidak satu pun dari contoh kode berikut yang berisi potensi kebocoran (tetapi kami akan kembali ke pertanyaan ini):



auto bar = std::shared_ptr<Bar>(new Bar);


auto bar = std::shared_ptr<Bar>(new Bar);
process(bar, foo());


process(std::shared_ptr<Bar>(new Bar));


Saya ulangi: untuk kebocoran potensial, Anda perlu menulis kode yang persis sama seperti pada contoh pertama - satu fungsi mengambil setidaknya dua parameter, salah satunya diinisialisasi dengan std :: shared_ptr yang baru dinamai, dan parameter kedua diinisialisasi dengan memanggil fungsi lain yang dapat membuang pengecualian.



Dan agar kebocoran memori potensial terjadi, diperlukan dua kondisi lagi:



  • sehingga kompiler mengocok panggilan dengan cara yang tidak menguntungkan;
  • sehingga fungsi mengevaluasi parameter kedua sebenarnya melempar pengecualian.


Kode berbahaya semacam itu tidak mungkin muncul lebih sering dari sekali dalam seratus penggunaan std :: shared_ptr.

Dan untuk mengimbangi bahaya ini, std :: shared_ptr didukung oleh penopang yang disebut std :: make_share.



Untuk mempermanis pil, Standar telah menambahkan yang berikut ini ke std :: make_ shared deskripsi:

Keterangan: Implementasi harus melakukan tidak lebih dari satu alokasi memori.


Catatan: Implementasi HARUS membuat tidak lebih dari satu alokasi memori.



Tidak, ini bukan jaminan.

Tetapi cppreference mengatakan bahwa semua implementasi yang diketahui melakukan hal itu.



Solusi ini ditujukan untuk meningkatkan kinerja dibandingkan dengan membuat std :: shared_ptr menggunakan panggilan konstruktor, yang memerlukan setidaknya dua alokasi: satu untuk menempatkan objek, dan satu untuk blok kontrol.



std :: make_share tidak berguna



Dimulai dengan c ++ 17, kebocoran memori dalam contoh langka yang rumit yang std :: make_share telah ditambahkan ke STL tidak lagi mungkin.



Tautan Studi:





Ada beberapa kasus lain di mana std :: make_share tidak berguna:



std :: make_share tidak akan dapat memanggil konstruktor pribadi
#include <memory>

class Bar
{
public:
    static std::shared_ptr<Bar> create()
    {
        // return std::make_shared<Bar>(); - no build
        return std::shared_ptr<Bar>(new Bar);
    }

private:
    Bar() = default;
};

int main()
{
    auto bar = Bar::create();

    return 0;
}




std :: make_share tidak mendukung penghapusan kustom
… variadic template. , , deleter.

std::make_shared_with_custom_deleter…



Ada baiknya untuk setidaknya belajar tentang masalah ini pada waktu kompilasi ...



std :: make_share berbahaya



Kami lulus dalam runtime.



operator kelebihan beban baru dan operator hapus akan diabaikan oleh std :: make_share
#include <memory>
#include <iostream>

class Bar
{
public:
    void* operator new(size_t)
    {
        std::cout << __func__ << std::endl;
        return ::new Bar();
    }

    void operator delete(void* bar)
    {
        std::cout << __func__ << std::endl;
        ::delete static_cast<Bar*>(bar);
    }
};

int main()
{
    auto bar = std::shared_ptr<Bar>(new Bar);
    // auto bar = std::make_shared<Bar>();

    return 0;
}


std::shared_ptr:

operator new

operator delete



std::make_shared:





Dan sekarang - hal yang paling penting dimana artikel itu sendiri dimulai.



Anehnya, fakta: bagaimana std :: shared_ptr akan menangani memori dapat sangat bergantung pada bagaimana itu dibuat - menggunakan std :: make_shared atau menggunakan konstruktor!



Mengapa ini terjadi?



Karena alokasi seragam "berguna" yang dihasilkan oleh std :: make_share memiliki efek samping yang melekat dari komunikasi yang tidak perlu antara blok kontrol dan objek yang dikelola. Mereka tidak bisa dibebaskan secara individual. Blok kontrol harus hidup selama setidaknya ada satu tautan lemah.



Std :: shared_ptr yang dibuat menggunakan konstruktor harus mengharapkan perilaku berikut:



  • alokasi objek yang dikelola (sebelum memanggil konstruktor, mis. di sisi pengguna);
  • alokasi unit kontrol;
  • setelah penghancuran referensi kuat terakhir - memanggil destruktor dari objek yang dikelola dan membebaskan memori yang didudukinya ; jika pada saat yang sama tidak ada satu pun tautan lemah - pelepasan unit kontrol;
  • pada penghancuran tautan lemah terakhir tanpa adanya tautan kuat - pelepasan blok kontrol.


Dan jika dibuat menggunakan std :: make_share:



  • alokasi objek yang dikelola dan unit kontrol;
  • setelah penghancuran referensi kuat terakhir - memanggil destruktor dari objek yang dikelola tanpa membebaskan memori yang didudukinya ; jika tidak ada tautan lemah tunggal, kemudian lepaskan blok kontrol dan memori objek yang dikelola;
  • β€” .


Membuat std :: shared_ptr dengan std :: make_shared memicu kebocoran ruang.



Mustahil untuk membedakan kapan runtime dengan tepat bagaimana instance std :: shared_ptr dibuat.



Mari kita beralih ke pengujian perilaku ini.



Ada cara yang sangat sederhana - gunakan std :: dialokasikanate_ shared dengan pengalokasi kustom, yang akan melaporkan semua panggilan ke sana. Tetapi tidak benar untuk mendistribusikan hasil yang diperoleh dengan cara ini ke std :: make_share.



Cara yang lebih benar adalah dengan mengontrol total konsumsi memori. Tetapi tidak ada pertanyaan tentang cross-platform di sini.



Kode untuk Linux, diuji pada Ubuntu 20.04 desktop x64. Siapa yang tertarik untuk mengulang ini untuk platform lain - lihat di sini (Eksperimen saya dengan macO telah menunjukkan bahwa opsi TASK_BASIC_INFO tidak melacak memori yang dibebaskan, dan TASK_VM_INFO_PURGEABLE adalah kandidat yang lebih baik).



Monitoring.h
#pragma once

#include <cstdint>

uint64_t memUsage();




Monitoring.cpp
#include "Monitoring.h"

#include <fstream>
#include <string>

uint64_t memUsage()
{
    auto file = std::ifstream("/proc/self/status", std::ios_base::in);
    auto line = std::string();

    while(std::getline(file, line)) {
        if (line.find("VmSize") != std::string::npos) {
            std::string toConvert;
            for (const auto& elem : line) {
                if (std::isdigit(elem)) {
                    toConvert += elem;
                }
            }
            return stoull(toConvert);
        }
    }

    return 0;
}




main.cpp
#include <iostream>
#include <array>
#include <numeric>
#include <memory>

#include "Monitoring.h"

struct Big
{
    ~Big()
    {
        std::cout << __func__ << std::endl;
    }

    std::array<volatile unsigned char, 64*1024*1024> _data;
};

volatile uint64_t accumulator = 0;

int main()
{
    std::cout << "initial: " << memUsage() << std::endl;

    auto strong = std::shared_ptr<Big>(new Big);
    // auto strong = std::make_shared<Big>();

    std::accumulate(strong->_data.cbegin(), strong->_data.cend(), accumulator);

    auto weak = std::weak_ptr<Big>(strong);

    std::cout << "before reset: " << memUsage() << std::endl;

    strong.reset();

    std::cout << "after strong reset: " << memUsage() << std::endl;

    weak.reset();

    std::cout << "after weak reset: " << memUsage() << std::endl;

    return 0;
}




Output konsol saat menggunakan std :: shared_ptr constructor:

awal: 5884

sebelum reset: 71424

~ Besar

setelah reset kuat: 5884

setelah reset lemah: 5884



Output konsol saat menggunakan std :: make_share:

awal: 5888

sebelum reset: 71428

~ Besar

setelah reset kuat: 71428

setelah reset lemah: 5888



Bonus



Namun, apakah mungkin untuk membocorkan memori sebagai hasil dari eksekusi kode



auto bar = std::shared_ptr<Bar>(new Bar);


?

Apa yang terjadi jika alokasi Bar berhasil, tetapi tidak ada cukup memori untuk blok kontrol?



Apa yang terjadi jika konstruktor dipanggil dengan custom deleter?



Bagian [util.smartptr.shared.const] dari Standar memastikan bahwa ketika pengecualian terjadi, di dalam konstruktor std :: shared_ptr:



  • untuk konstruktor tanpa deleter khusus, pointer yang lewat akan dihapus menggunakan hapus atau hapus [];
  • untuk konstruktor dengan deleter khusus, pointer yang lewat akan dihapus menggunakan deleter ini sendiri.


Tidak ada kebocoran yang dijamin oleh Standar.



Sebagai hasil dari pembacaan sepintas implementasi dalam tiga kompiler (Apple clang versi 11.0.3, GCC 9.3.0, MSVC 2019 16.6.2), saya dapat mengkonfirmasi bahwa ini adalah masalahnya.



Keluaran



Dalam c ++ 11 dan c ++ 14, kerugian dari menggunakan std :: make_share dapat diseimbangkan dengan satu-satunya fungsi yang bermanfaat.



Dimulai dengan c ++ 17, aritmatika sama sekali tidak mendukung std :: make_share.



Situasinya mirip dengan std :: mengalokasikan_bagi.



Banyak hal di atas berlaku untuk std :: make_unique juga, tetapi tidak terlalu membahayakan.



All Articles