C ++ 'da tür bilgileri ne zaman geriye doğru akar?


92

Az önce Stephan T. Lavavej'in CppCon 2018"Sınıf Şablonu Argüman Çıkarımı " üzerine konuşmasını izledim , burada bir noktada tesadüfen şöyle diyor:

C ++ türünde bilgi neredeyse hiçbir zaman geriye doğru akmaz ... "Neredeyse" demek zorunda kaldım çünkü bir veya iki durum var, muhtemelen çok ama çok az .

Hangi davalardan bahsettiğini anlamaya çalışmasına rağmen, hiçbir şey bulamadım. Dolayısıyla soru:

Hangi durumlarda C ++ 17 standardı bu tür bilginin geriye doğru yayılmasını zorunlu kılar?


model eşleştirme kısmi uzmanlaşma ve yıkıcı görevler.
v.oddou

Yanıtlar:


80

İşte en az bir durum:

struct foo {
  template<class T>
  operator T() const {
    std::cout << sizeof(T) << "\n";
    return {};
  }
};

eğer bunu yaparsanız foo f; int x = f; double y = f;, içeride ne Tolduğunu anlamak için bilgi "geriye doğru" akacaktır operator T.

Bunu daha gelişmiş bir şekilde kullanabilirsiniz:

template<class T>
struct tag_t {using type=T;};

template<class F>
struct deduce_return_t {
  F f;
  template<class T>
  operator T()&&{ return std::forward<F>(f)(tag_t<T>{}); }
};
template<class F>
deduce_return_t(F&&)->deduce_return_t<F>;

template<class...Args>
auto construct_from( Args&&... args ) {
  return deduce_return_t{ [&](auto ret){
    using R=typename decltype(ret)::type;
    return R{ std::forward<Args>(args)... };
  }};
}

yani şimdi yapabilirim

std::vector<int> v = construct_from( 1, 2, 3 );

ve çalışıyor.

Tabii ki, neden sadece yapmıyorsunuz {1,2,3}? Eh, {1,2,3}bir ifade değildir.

std::vector<std::vector<int>> v;
v.emplace_back( construct_from(1,2,3) );

ki kuşkusuz, biraz daha sihirbazlık gerektiriyor: Canlı örnek . (F'nin SFINAE kontrolünü yaparak sonuçtan çıkarma dönüşünü yapmalı, ardından F'yi SFINAE dostu yapmalı ve deduce_return_t operatörü T'de std :: initializer_list'i engellemeliyim.)


Çok ilginç cevap ve yeni bir numara öğrendim, bu yüzden çok teşekkür ederim! Örneğinizi derlemek için bir şablon kesinti kılavuzu eklemem gerekiyordu , ancak bunun dışında bir cazibe gibi çalışıyor!
Massimiliano

5
Üzerindeki &&niteleyici operator T()harika bir dokunuş; burada kötüye kullanılırsa autobir derleme hatasına neden olarak zayıf etkileşimden kaçınmaya yardımcı olur auto.
Justin

1
Bu çok etkileyici, beni bir referansa yönlendirebilir misin / örnekteki fikirden bahsedebilir misin? ya da belki orijinal :) ...
llllllllll

3
@lili Hangi fikir? 5 sayıyorum: İade türlerini çıkarmak için T operatörünü kullanma? Çıkarılan türü bir lambda'ya geçirmek için etiketleri mi kullanıyorsunuz? Kendi yerleşim nesnesi yapınızı döndürmek için dönüştürme operatörlerini kullanıyor musunuz? 4'ünün tümü bağlanıyor mu?
Yakk - Adam Nevraumont

1
@lili Tha "daha gelişmiş yol" örneği, dediğim gibi, sadece 4 ya da daha fazla fikir birbirine yapıştırılmış. Bu yazı için anında yapıştırmayı yaptım, ancak kesinlikle birlikte kullanılanların birçok çiftini hatta üçünü gördüm. Bu, makul ölçüde belirsiz teknikler (tootsie'nin şikayet ettiği gibi), ancak yeni bir şey değil.
Yakk - Adam Nevraumont

31

Stephan T.Lavavej bir tweet ile bahsettiği vakayı şöyle anlattı :

Düşündüğüm durum, aşırı yüklenmiş / şablonlu bir fonksiyonun adresini nereden alabileceğiniz ve belirli bir tipteki bir değişkeni başlatmak için kullanılıyorsa, hangisini istediğinizi belirsizleştirecektir. (Açıklayıcı olan şeylerin bir listesi var.)

Bunun örneklerini cppreference sayfasından aşırı yüklenmiş fonksiyonun adresi üzerinde görebiliriz , aşağıda birkaçını hariç tuttum:

int f(int) { return 1; } 
int f(double) { return 2; }   

void g( int(&f1)(int), int(*f2)(double) ) {}

int main(){
    g(f, f); // selects int f(int) for the 1st argument
             // and int f(double) for the second

     auto foo = []() -> int (*)(int) {
        return f; // selects int f(int)
    }; 

    auto p = static_cast<int(*)(int)>(f); // selects int f(int)
}

Michael Park ekliyor :

Somut bir türü başlatmakla da sınırlı değildir. Ayrıca argümanların sayısından da çıkarım yapabilir

ve şu canlı örneği sağlar :

void overload(int, int) {}
void overload(int, int, int) {}

template <typename T1, typename T2,
          typename A1, typename A2>
void f(void (*)(T1, T2), A1&&, A2&&) {}

template <typename T1, typename T2, typename T3,
          typename A1, typename A2, typename A3>
void f(void (*)(T1, T2, T3), A1&&, A2&&, A3&&) {}

int main () {
  f(&overload, 1, 2);
}

Ben burada biraz daha detaylandırıyorum .


4
Bunu şu şekilde de tanımlayabiliriz: Bir ifadenin türünün bağlama bağlı olduğu durumlar?
MM

20

Aşırı yüklenmiş fonksiyonların statik dökümüne inanıyorum, akışın normal aşırı yük çözünürlüğünde olduğu gibi ters yönde gittiğine inanıyorum. Yani bunlardan biri geriye doğru sanırım.


7
Bunun doğru olduğuna inanıyorum. Ve bir işlev adını bir işlev işaretçi türüne ilettiğiniz zamandır; tür bilgisi, hangi aşırı yüklemenin seçildiğini belirlemek için ifadenin bağlamından (atadığınız / oluşturduğunuz / vb. tür) işlevin adına doğru akar.
Yakk - Adam Nevraumont
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.