Mengapa std :: function tidak berpartisipasi dalam resolusi kelebihan beban?

17

Saya tahu bahwa kode berikut tidak akan dikompilasi.

void baz(int i) { }
void baz() {  }


class Bar
{
    std::function<void()> bazFn;
public:
    Bar(std::function<void()> fun = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Karena std::functiondikatakan tidak mempertimbangkan resolusi overload, seperti yang saya baca posting lain ini .

Saya tidak sepenuhnya memahami keterbatasan teknis yang memaksa solusi semacam ini.

Saya membaca tentang fase - fase terjemahan dan template pada cppreference, tetapi saya tidak dapat memikirkan alasan apa pun yang saya tidak dapat menemukan contoh tandingannya. Dijelaskan kepada orang awam (masih baru di C ++), apa dan selama tahap terjemahan mana yang membuat gagal untuk dikompilasi?

TuRtoise
sumber
1
@ Evg tetapi hanya ada satu kelebihan yang masuk akal untuk diterima dalam contoh OPs. Dalam contoh Anda, keduanya akan membuat pertandingan
idclev 463035818

Jawaban:

13

Ini tidak benar-benar ada hubungannya dengan "fase terjemahan". Ini murni tentang konstruktorstd::function .

Lihat, std::function<R(Args)>tidak mengharuskan fungsi yang diberikan persis dari jenisnya R(Args). Secara khusus, itu tidak mengharuskan diberi fungsi pointer. Ia dapat mengambil tipe apa pun yang dapat dipanggil (pointer fungsi anggota, beberapa objek yang memiliki kelebihan operator()) asalkan tidak dapat dimasuki seolah-olah mengambil Argsparameter dan mengembalikan sesuatu yang dapat dikonversi ke R (atau jikaR ini void, dapat kembali apa-apa).

Untuk melakukan itu, konstruktor yang sesuai std::functionharus berupa templat : template<typename F> function(F f);. Artinya, dapat mengambil jenis fungsi apa pun (tunduk pada pembatasan di atas).

Ekspresi bazmewakili set overload. Jika Anda menggunakan ungkapan itu untuk memanggil set overload, itu tidak masalah. Jika Anda menggunakan ekspresi itu sebagai parameter ke fungsi yang mengambil penunjuk fungsi tertentu, C ++ dapat mengurangi overload yang ditetapkan ke satu panggilan, sehingga membuatnya baik-baik saja.

Namun, begitu suatu fungsi adalah templat, dan Anda menggunakan deduksi argumen templat untuk mengetahui parameter apa itu, C ++ tidak lagi memiliki kemampuan untuk menentukan apa kelebihan beban yang benar dalam kumpulan kelebihan tersebut. Jadi, Anda harus menentukannya secara langsung.

Nicol Bolas
sumber
Maafkan saya jika ada sesuatu yang salah, tetapi mengapa C ++ tidak memiliki kemampuan untuk membedakan kelebihan yang tersedia? Saya melihat sekarang bahwa std :: function <T> menerima semua jenis yang kompatibel, tidak hanya kecocokan persis, tetapi hanya salah satu dari dua baz () overload yang tidak dapat dimasuki seolah-olah mengambil parameter yang ditentukan. Mengapa tidak mungkin untuk ambigu?
TuRtoise
Karena semua yang dilihat oleh C ++ adalah tanda tangan yang saya kutip. Dia tidak tahu apa yang seharusnya cocok dengannya. Pada dasarnya, setiap kali Anda menggunakan kelebihan set terhadap apa pun yang tidak jelas apa jawaban yang benar secara eksplisit dari kode C ++ (kode tersebut adalah deklarasi templat), bahasa memaksa Anda untuk mengeja apa yang Anda maksudkan.
Nicol Bolas
1
@TuRtoise: Parameter templat pada functiontemplat kelas tidak relevan . Yang penting adalah parameter template pada konstruktor yang Anda panggil. Yang mana saja typename F: alias, tipe apa saja.
Nicol Bolas
1
@TuRtoise: Saya pikir Anda salah paham tentang sesuatu. Ini "hanya F" karena itulah cara kerja templat. Dari tanda tangan, konstruktor fungsi itu mengambil tipe apa pun, jadi setiap upaya untuk menyebutnya dengan pengurangan argumen templat akan menyimpulkan tipe dari parameter. Setiap upaya untuk menerapkan deduksi ke set kelebihan beban adalah kesalahan kompilasi. Kompiler tidak mencoba untuk menyimpulkan setiap jenis yang mungkin dari set untuk melihat mana yang berhasil.
Nicol Bolas
1
"Setiap upaya untuk menerapkan pengurangan ke set kelebihan beban adalah kesalahan kompilasi. Kompilator tidak mencoba menyimpulkan setiap jenis yang mungkin dari set untuk melihat mana yang berhasil." Persis apa yang saya lewatkan, terima kasih :)
TuRtoise
7

Resolusi kelebihan terjadi hanya ketika (a) Anda memanggil nama fungsi / operator, atau (b) melemparkannya ke pointer (berfungsi atau fungsi anggota) dengan tanda tangan eksplisit.

Tidak ada yang terjadi di sini.

std::function mengambil objek apa pun itu kompatibel dengan tanda tangannya. Tidak memerlukan pointer fungsi secara khusus. (lambda bukan fungsi std, dan fungsi std bukan lambda)

Sekarang dalam varian fungsi homebrew saya, untuk tanda tangan R(Args...) saya juga menerima R(*)(Args...)argumen (pencocokan persis) untuk alasan ini. Tetapi itu berarti meningkatkan tanda tangan "kecocokan tepat" di atas tanda tangan "kompatibel".

Masalah intinya adalah bahwa set overload bukan objek C ++. Anda dapat memberi nama kumpulan kelebihan, tetapi Anda tidak dapat memutarnya "secara bawaan".

Sekarang, Anda dapat membuat set fungsi pseudo-overload seperti ini:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__(decltype(args)(args)...) )

ini menciptakan objek C ++ tunggal yang dapat melakukan resolusi kelebihan pada nama fungsi.

Memperluas makro, kita mendapatkan:

[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }

yang menjengkelkan untuk menulis. Versi yang lebih sederhana, hanya sedikit kurang bermanfaat, ada di sini:

[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }

kami memiliki lambda yang membutuhkan sejumlah argumen, kemudian menyempurnakannya baz .

Kemudian:

class Bar {
  std::function<void()> bazFn;
public:
  Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};

bekerja. Kami menunda resolusi kelebihan ke dalam lambda yang kami simpan fun, alih-alih lewatfun set kelebihan beban secara langsung (yang tidak dapat diselesaikan).

Setidaknya ada satu proposal untuk mendefinisikan operasi dalam bahasa C ++ yang mengubah nama fungsi menjadi objek set yang kelebihan. Sampai proposal standar tersebut ada dalam standar, OVERLOADS_OFmakro berguna.

Anda bisa melangkah lebih jauh, dan mendukung cast-to-compatible-function-pointer.

struct baz_overloads {
  template<class...Ts>
  auto operator()(Ts&&...ts)const
  RETURNS( baz(std::forward<Ts>(ts)...) );

  template<class R, class...Args>
  using fptr = R(*)(Args...);
  //TODO: SFINAE-friendly support
  template<class R, class...Ts>
  operator fptr<R,Ts...>() const {
    return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
  }
};

tapi itu mulai tumpul.

Contoh langsung .

#define OVERLOADS_T(...) \
  struct { \
    template<class...Ts> \
    auto operator()(Ts&&...ts)const \
    RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
    template<class R, class...Args> \
    using fptr = R(*)(Args...); \
\
    template<class R, class...Ts> \
    operator fptr<R,Ts...>() const { \
      return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
    } \
  }
Yakk - Adam Nevraumont
sumber
5

Masalahnya di sini adalah tidak ada yang memberitahu kompiler bagaimana melakukan fungsi untuk membusuk pointer. Jika Anda memiliki

void baz(int i) { }
void baz() {  }

class Bar
{
    void (*bazFn)();
public:
    Bar(void(*fun)() = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Kemudian kode akan berfungsi karena sekarang kompiler mengetahui fungsi yang Anda inginkan karena ada jenis konkret yang Anda tetapkan.

Ketika Anda menggunakan std::functionAnda menyebutnya pembangun objek fungsi yang memiliki bentuk

template< class F >
function( F f );

dan karena itu adalah templat, ia perlu menyimpulkan jenis objek yang dilewatkan. karena bazmerupakan fungsi kelebihan tidak ada tipe tunggal yang dapat dideduksi sehingga deduksi template gagal dan Anda mendapatkan kesalahan. Anda harus menggunakan

Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}

untuk mendapatkan kekuatan satu jenis dan memungkinkan deduksi.

NathanOliver
sumber
"Karena baz adalah fungsi kelebihan beban tidak ada tipe tunggal yang dapat disimpulkan" tetapi karena C ++ 14 "konstruktor ini tidak berpartisipasi dalam resolusi kelebihan kecuali f dapat dipanggil untuk tipe argumen Args ... dan mengembalikan tipe R" Saya tidak yakin, tapi saya hampir berharap bahwa ini akan cukup untuk menyelesaikan ambiguitas
idclev 463035818
3
@ mantanlyknownas_463035818 Tetapi untuk menentukan itu, perlu menyimpulkan jenis pertama, dan itu tidak bisa karena itu adalah nama yang kelebihan beban.
NathanOliver
1

Pada titik ini kompiler memutuskan overload mana yang akan masuk ke std::functionkonstruktor yang diketahuinya adalah bahwa std::functionkonstruktor tersebut dibuat untuk mengambil tipe apa pun. Itu tidak memiliki kemampuan untuk mencoba kedua kelebihan dan menemukan bahwa yang pertama tidak dikompilasi tetapi yang kedua tidak.

Cara untuk mengatasinya adalah dengan secara eksplisit memberi tahu kompiler mana yang kelebihan Anda inginkan dengan static_cast:

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
Alan Birtles
sumber