天天看點

發現大師們的錯誤

Lippman 「C++ Primer」 P537

享譽世界的C++經典「C++ Primer」(第五版)在介紹子類的虛函數時,說一個派生類的函數如果覆寫了(或者說試圖重寫,override)某個繼承而來的虛函數,則它的形參類型必須與被它覆寫的基類函數完全一緻。這話當然沒問題,緊接着他說,同樣,派生類中虛函數的傳回類型也必須與基類函數比對。該規則存在一個例外,當類的虛函數傳回類型是類本身的指針或引用時,上述規則有效。

問題恰出現在後半句,這句話是對協變傳回類型(covariant return type)的刻闆了解和窄化,關于協變傳回類型,更詳細的内容請見 C++基礎::文法特性::函數重寫(override)與協變傳回類型(covariant return type)。

協變傳回類型要求子類重寫的虛函數的傳回值(指針或引用類型)與父類被重寫的虛函數之間構成繼承關系,并不要求傳回類本身的指針或引用,

#include <iostream>
class A{};
class B: public A{};

class C
{
public:
    virtual A* foo()
    {
        std::cout << "C::foo()" << std::endl;
        return new A;
    }
};

class D: public C
{
public:
    virtual B* foo()
    {
        std::cout << "D::foo()" << std::endl;
        return new B;
    }
};

int main(int, char**)
{
    C c; D d;
    c.foo();
    d.foo();
    return ;
}
           

并不要求構成重載的虛函數之間,它們的傳回值是自身的指針或者引用。

控制台輸出為:

C::foo()
D::foo()
           

編譯通過,運作通過!

當然,我這麼做是吹毛求疵、咬文嚼字的表現,這麼做并無十分明确的意義,隻是大師們在這裡并未言明協變傳回類型,其實是對協變傳回類型的窄化和呆闆認識。

侯捷STL源碼剖析——for_each與transform

在介紹for_each算法的源碼時,

template<typename InputIterator, typename Function>
Function for_each(InputIterator first, InputIterator last, Function f)
{
    for(; first != last; ++first)
        f(*first);
    return f;
}
           

侯捷老師說,“将仿函數f施行于[first, last)區間内的每一個元素身上,f不可以改變元素内容,因為first和last都是InputIterator”。侯捷老師接着又說,“如果想要一一修改元素内容,應該使用算法transform”,言下之意就是for_each與transform的不同在于前者不對元素内容進行修改,而後者進行了修改。可是如果for_each沒修改元素内容的話,函數傳回的是仿函數,那麼函數的目的是什麼呢?

而且在實際中:

class Item
{
private:
    std::string _name;
    float _price;
public:
    Item(const std::string& name, float price):_name(name), _price(price){}
    std::string getName() const { return _name;}
    void setName(const std::string& name) { _name = name;}
    float getPrice() const { return _price;}
    void setPrice(float price) { _price = price;}
};

int main(int, char**)
{
    tyepdef std::shared_ptr<Item> ItemPtr;
    std::vector<ItemPtr> books {ItemPtr(new Item("C++", )), ItemPtr(new Item("Python", )), ItemPtr(new Item("Machine Learning", ))};


    // 這時如果我們想商品的價格在原價的基礎上再加5,使用for_each算法
    std::for_each(books.begin(), books.end(), [](ItemPtr& elem){elem->setPrice(elem->getPrice()+)});
                    // 第一:傳遞給lambda函數的實參是一個引用類型,
                    // 第二:對原有的元素内容進行了修改
    for (const auto& elem: books)
        std::cout << elem->getName() << ": " << elem->getPrice() << std::endl;

    return ;
}

// 我們再來看transform的做法
int main(int, char**)
{
    tyepdef std::shared_ptr<Item> ItemPtr;
    std::vector<ItemPtr> books {ItemPtr(new Item("C++", )), ItemPtr(new Item("Python", )), ItemPtr(new Item("Machine Learning", ))};

    std::vector<ItemPtr> books2(books.size());
    std::transform(books.begin(), books.end(), books2.begin(), 
                    [](ItemPtr elem){ return ItemPtr(new(elem->getName(), elem->getPrice()+));});
    for (const auto& elem: books)
        std::cout << elem->getName() << ": " << elem->getPrice() << std::endl;
    for (const auto& elem: books)
        std::cout << elem->getName() << ": " << elem->getPrice() << std::endl;
                    // transform所做的工作是,将仿函數施加輸入序列後傳回給輸出序列
                    // 我們看到傳遞給transform的仿函數的參數是一個對象value語義,而非reference 
}
           

如上代碼我們可以看到,真正對輸入序列進行修改的不僅不是transform,而是for_each,因為for_each的輸入是單獨的一個序列,而transform的輸入是兩個輸入序列,也即将仿函數施行在一個輸入序列得到的結果再傳回給另一個序列。而且,傳遞給for_each的仿函數對象的參數是引用類型(swap(int, int)沒有意義),傳遞給transform的是value 語義。

在STL的語言環境範疇裡,直接改變元素值(如for_each),或者複制元素到另一個區間的過程中改變元素值(如transform,原區間不發生變化)都屬于更易型算法(modifying algorithm)。

我們可以繼續探索二者的差別,因為是将操作的傳回值賦予元素,而不是直接改動元素,transform的速度稍慢些,不過其靈活性更高,因為它可以把某個序列複制到标的序列(目标序列),同時改動元素内容。

我們再來看新标準下的for_each,for_each算法非常靈活,它允許以不同的方式通路(可以對元素不進行修改)、處理和修改每一區間内的元素,然而,自C++11起,for_each恐将日益喪失其重要性,因為十分友善和強大的

range-based for

循環:

// 同樣是為每一個商品的價錢+
for (auto& elem: books)
    elem->setPrice(elem->getPrice() + );