• hello world
    2019-12-02
    一直有个问题想问老师,老师第一节也说了一些,但我还是有一些疑惑和焦虑希望老师能够解惑。
    现在C++的应用范围貌似越来越窄,以前可能很多后台会用到,但是现在貌似后台都是go和java多一些,真正追求性能极致的貌似不多,现在也就一些人工智能方面和嵌入式方面会用到,但也是少数,真正要做一个深度学习框架的也不太多,即便是大企业也是个别几个部门,作为一个C++程序员比较迷茫,感觉这条路比较窄,未来的路怎么走呢?

    作者回复: 把C++当作一种工具,也当作一种锻炼思维的方式,不要把它当成非用不可的圣器。把自己定位成“程序员”,而不是“C++程序员”。职业的将来方向定位成“软件架构师”、“开发主管”、“CTO”之类的角色,而不是“高级C++程序员”。

    该用其他语言的时候用其他语言。要看到,即使不用C++,C++程序员在对优化的理解、对内存管理的理解等方面都是具有很大优势的。

     2
     18
  • NEVER SETTLE
    2019-12-02
    请教下老师,字符串字面量是左值,是不是在C++中 字符串其实是const char[N],其实是个常量表达式,在内存中有明确的地址。

    作者回复: 是。👍

     1
     11
  • 中年男子
    2019-12-03
    第二题:
    左值和右值都有效是因为构造参数时,如果是左值,就用拷贝构造构造函数,右值就用移动构造函数
    无论是左值还是右值,构造参数时直接生成新的智能指针,因此不需要判断

    作者回复: 理解满分。👍

    
     9
  • 糖
    2019-12-02
    又是看不懂的一节。。。老师讲的课程太深刻了。。。
    1. 本来感觉自己还比较了解左右值的区别,但是,文中提到:一个 lvalue 是通常可以放在等号左边的表达式,左值,然后下面说:字符串字面量如 "hello world",但字符串字面量貌似不可以放到等号左边,搞晕了。
    2. 内存访问的局域性是指什么呢?又有何优势呢?老师能提供介绍的链接吗
    3. 为何对于移动构造函数来讲不抛出异常尤其重要呢?
    希望老师能指点一下
    展开

    作者回复: 1. “通常”。字符串字面量是个继承自C的特殊情况。

    2. 这个搜索一下就行。这是CPU的缓存结构决定的。

    3. 其他类,尤其容器类,会期待移动构造函数无异常,甚至会在它有异常时选择拷贝构造函数,以保证强异常安全性。

     3
     8
  • 禾桃
    2019-12-03
    "请查看一下标准函数模板 make_shared 的声明,然后想一想,这个函数应该是怎样实现的。"

    template <class T, class... Args>
    std::shared_ptr<T> make_shared (Args&&... args)
    {
        T* ptr = new T(std::forward<Args...>(args...));
        return std::shared_ptr<T>(ptr);
    }

    我的考虑是:
    make_shared声明里的(Args&&...) 是universal reference, 所以在函数体里用完美转发(std::forward)把参数出入T的构造函数, 以调用每个参数各自对用的构造函数(copy or move)。

    肯定还有别的需要考量的地方,请指正。

    谢谢!
    展开

    作者回复: 对,最主要就是这点,用完美转发来正确调用构造函数。

    
     6
  • NEVER SETTLE
    2019-12-03
    老师,留言有字数限制,我是接着上个留言来的,上面那个总结了下左值与右值,这个是右值引用的学习心得:
    ## 2、右值引用
    **针对以上的所说的“临时变量”,如何来“接收”它呢?**
    * 最直白的办法,就是直接用一个变量来“接收”
    以x++为例:
    ```
    void playVal(int y) {
        cout << "y = " << y << ", (y).adrr = " << &y << endl;
    }

    int x = 0;
    playVal(x++);
    cout << "x = " << x << ", (x).adrr = " << &x << endl;
    ```

    >运行结果:
    y = 0, (y).adrr = 0x22fe20
    x = 1, (x).adrr = 0x22fe4c

    这是一个值传递过程,相当于 int y = x++,即x++生成的临时变量给变量y赋值,之后临时变量就“消失”,这里发生是一次拷贝。

    如何避免发生拷贝呢?
    通常做法是使用引用来“接收”,即引用传递。
    上面说过,使用一个(常规)引用来“接收”一个临时变量,会报错:
    ```
    void playVal(int& y)
    ```
    > error : 非常量引用的初始值必须为左值

    * 普遍的做法都是使用常量引用来“接收”临时变量(C++11之前)
    ```
    void playVal(const int& y)
    ```
    这里编译器做了处理: int tmp = x++; const int& y = tmp; 发生内存分配。
    其实还是发生了拷贝。

    * 使用右值引用来“接收”临时变量(C++11之后)
    上面说过,“临时变量”是一个右值,所以这里可以使用右值引用来“接收”它
    右值引用的形式是 T&& :
    ```
    void playVal(int&& y) {
        cout << "y = " << y << ", (y).adrr = " << &y << endl;
    }

    int x = 0;
    playVal(x++);
    cout << "x = " << x << ", (x).adrr = " << &x << endl;
    ```

    > 运行结果:
    y = 0, (y).adrr = 0x22fe4c
    x = 1, (x).adrr = 0x22fe48

    这是一个(右值)引用传递的过程,相当于 int&& y = x++,这里的右值引用 y 直接“绑定”了“临时变量”,因为它就会有了命名,变成“合法”的,就不会“消失”。
    **注意:这里的变量 y 虽然是右值引用类型,但它是一个左值,可以正常对它取地址**
    (如上例所示)
    展开

    作者回复: 再多说一句,如果每个人都这么让我来看笔记的话,我是不可能满足所有人的。只看你的,也对别人不公。在这儿回答(不重复的)问题则不同,问题一般是有共性的,回答之后,大家都能看到,都能从中受益。

     1
     5
  • NEVER SETTLE
    2019-12-03
    老师,我这个初学者看的比较慢,目前只看了右值与右值引用,下面是我总结了的学习心得,请您指点下:
    **背景:
    C++11为了支持移动操作,引用了新的引用类型-右值引用。
    所谓右值引用就是绑定到右值的引用。
    为了区分右值引用,引入左值引用概念,即常规引用。
    那左值与右值是是什么?**

    ## 1、左值与右值

    **左值 lvalue 是有标识符、可以取地址的表达式**
    * 变量、函数或数据成员的名字
    * 返回左值引用的表达式,如 ++x、x = 1、cout << ' '
    * 字符串字面量如 "hello world"

    表达式是不是左值,就看是否可以取地址,或者返回类型是否可以用(常规)引用来接收:

    ```
    int x = 0;
    cout << "(x).addr = " << &x << endl;
    cout << "(x = 1).addr = " << &(x = 1) << endl; //x赋值1,返回x
    cout << "(++x).addr = " << &++x << endl; //x自增1,返回x
    ```

    > 运行结果:
    (x).addr = 0x22fe4c
    (x = 1).addr = 0x22fe4c
    (++x).addr = 0x22fe4c

    ```
    cout << "hello world = " << &("hello world") << endl;
    ```

    > 运行结果:
    hello world = 0x40403a

    C++中的字符串字面量,可以称为字符串常量,表示为const char[N],其实是地址常量表达式。
    在内存中有明确的地址,不是临时变量。

    ```
    cout << "cout << ' ' = " << &(cout << ' ') << endl;
    ```
    > 运行结果:
    cout << ' ' = 0x6fd0acc0

    **纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般称为“临时对象”**
    * 返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
    * 除字符串字面量之外的字面量,如 42、true

    ```
    //cout << "(x++).addr = " << &x++ << endl; //返回一个值为x的临时变量,再把x自增1
    //cout << "(x + 1).addr = " << &(x + 1) << endl; //返回一个值为x+1的临时变量
    //cout << "(42).addr = " << &(42) << endl; //返回一个值为42的临时变量
    //cout << "(true).addr = " << &(true) << endl; //返回一个值为true的临时变量
    ```
    > 编译出错:
    每行代码报错:表达式必须为左值或函数指示符
    因为以上表达式都返回的是“临时变量”,是不可以取地址的

    ---
    展开

    作者回复: 你在学校的时候,都是让老师来看你的笔记记得好不好的吗?😂

    对不起,如果有明确的问题,我可以回答。否则,我只能暂时忽略了。

    
     4
  • NEVER SETTLE
    2019-12-02
    “返回左值引用的表达式,,如 x++、x + 1 ”不太清楚原因,后来我就试了下:

    ```
    int x = 0;
    cout << "(x).addr = " << &x << endl;
    cout << "(x = 1).addr = " << &(x = 1) << endl;
    cout << "(++x).addr = " << &++x << endl;
    //cout << "(x++).addr = " << &x++ << endl;
    ```

    > 运行结果:
    (x).addr = 0x22fe4c
    (x = 1).addr = 0x22fe4c
    (++x).addr = 0x22fe4c
    最后一行注释掉的代码报错:表达式必须为左值或函数指示符
    展开

    作者回复: 对,x = 1 和 ++x 返回的都是对 x 的 int&。x++ 则返回的是 int。

    
     4
  • 哇咔咔
    2019-12-11
    老师你好,这段代码压测下来,发现左值引用没有性能的提升。压测时间对比是:
    elapsed time: 1.2184s
    elapsed time: 1.1857s

    请问为什么呢?

    #include <string>
    #include <ctime>
    #include <chrono>
    #include <iostream>

    void func1(std::string s)
    {
    }

    void func2(const std::string &s)
    {
    }

    void test2()
    {
        auto start = std::chrono::system_clock::now();
        for (size_t i = 0; i < 20000000; i++)
        {
            func1(std::string("hello"));
        }
        auto end = std::chrono::system_clock::now();
        std::chrono::duration<double> elapsed_seconds = end - start;
        std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";

        start = std::chrono::system_clock::now();
        for (size_t i = 0; i < 20000000; i++)
        {
            func2(std::string("hello"));
        }
        end = std::chrono::system_clock::now();
        elapsed_seconds = end - start;
        std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
    }

    int main()
    {
        test2();
    }
    展开

    作者回复: 因为移动发挥威力了……试试把 std::string("hello") 放到 test2 开头作为变量,然后后面使用这个变量。

     2
     2
  • 宝林
    2019-12-05
    这一节看了三遍,思路太跳跃了,不易读
    
     2
  • 千鲤湖
    2019-12-04
    老师,我把实例稍微改了下,
    class Obj
      {
        public:
          Obj()
          {
              std::cout << "Obj()" << std::endl;
          }
      
          Obj(const Obj&)
          {
              std::cout << "Obj(const Obj&)" << std::endl;
          }
      
          Obj(Obj&&)
          {
              std::cout << "Obj(Obj&&)" << std::endl;
          }
      };

    void foo(const Obj&)
    void foo(Obj&&)
    void bar(const Obj& s)
    void bar(Obj&& s)

    int main()
    {
       bar(Obj());
    }

    构造函数内加了打印。

    期望看到的结果是这样的
    Obj()
    Obj(&&)
    bar(Obj&&)
    Obj(const&)
    foo(const Obj&)
    可实际输出如下
    Obj()
    bar(&&)
    foo(const &)

    并没有期望中的移动构造和复制构造,这是为什么啊。
    关于没有移动构造,我的理解是Obj()本来已经是个右值了,不必再构造。
    可是想不通为什么没有了复制构造。
    展开

    作者回复: 中间传的都是引用,没有拷贝或移动发生的。只有用Obj(而不是Obj&或Obj&&)作为参数类型才会发生拷贝或移动构造。

    
     2
  • 安静的雨
    2019-12-03
    Obj simple_with_move()
    {
        Obj obj;
        // move 会禁止 NRVO
        return std::move(obj);
    }

    move后不是类型转换到右值引用了吗? 为啥返回值类型还是obj?
    展开

    作者回复: 文中已经说了,禁止返回本地对象的引用。

    需要生成一个 Obj,给了一个 Obj&&,不就是调用构造函数而已么。所以(看文中输出),就是多产生了一次Obj(Obj&&) 的调用。

     2
     2
  • 不谈
    2019-12-03
    感觉左值和右值有点像Java里的值传递和引用传递,但又不完全像,还要再看几遍,好好理解理解

    作者回复: 不是。Java里没有右值,也不必要有右值。因为Java里的对象都是引用语义,从来不是值语义。再仔细看一下。

     1
     1
  • 罗乾林
    2019-12-02
    平时Java是主要使用语言,也来回答一下
    1、make_shared 创建(new)新对象根据传入的值类别调用拷贝构造或移动构造,然后将新对象的指针给shared_ptr,其中我看见了_Types&&和forward
    2、smart_ptr::operator= 中参数为值传递,会先调用smart_ptr的拷贝构造函数,生成了临时对象,然后调用swap,
    因为生成了新对象所以对等号两边是否引用同一对象进行判断,也没意义了,但是a=a也会有临时对象的产生,有性能开销

    有错误的方,望老师指正
    展开

    作者回复: 2完全正确。1再想想。🤓

    
     1
  • 晚风·和煦
    2020-02-04
    老师,string那里的第三点是不是应该为临时对象😂

    作者回复: 联合第四点一起看。对象 3 一般不是临时对象。

    
    
  • 晚风·和煦
    2020-02-04
    老师,string那里的复制1次,复制2次没太懂什么意思😨

    作者回复: 就是字符串指向的内存区域被类似于 strcpy 或 memcpy 的调用复制了几次。

    
    
  • 王旧业
    2020-02-01
    NRVO行为的例子跟编译器的优化参数也有关系,在MSVC下关闭优化/Od,看不到NRVO,只能看到3类情况下都使用的是移动构造

    作者回复: 是这样的。在第 10 讲中有一个更完整的讨论,你也可以参考一下。

    
    
  • Geek_9e42eb
    2020-01-07
    左值 lvalue 是有标识符、可以取地址的表达式,字符串常量(如“hello world”)可以取地址吗?字符串常量(如“hello world”)应该不可以放在等号的左边吧?为什么说它也是左值?

    作者回复: 试试不就知道了?😉

    字符串常量是常左值,有确定的地址。常左值能不能放等号左边呢?我不需要回答了吧。

     1
    
  • Jason
    2020-01-06
    老师,感谢您的回复。prvalue应该是test1返回的临时变量吧,但是a是一个左值,编译器根据return来判断此时优先使用move而不是copy来返回吗?

    作者回复: 直观理解,函数已经要返回了,本地对象已经没用了,编译器选择最有效的返回数据方式:第一是返回值优化,第二是移动,第三才是拷贝。

    这就是我文中所写的:“在 C++11 之前,返回一个本地对象意味着这个对象会被拷贝,除非编译器发现可以做返回值优化(named return value optimization,或 NRVO),能把对象直接构造到调用者的栈上。从 C++11 开始,返回值优化仍可以发生,但在没有返回值优化的情况下,编译器将试图把本地对象移动出去,而不是拷贝出去。”

    
    
  • Jason
    2020-01-06

    老师,我A类里定义了拷贝构造和移动构造,在关闭返回值优化后,test1中返回了一个左值,但是打印结果却是调用了移动构造,不应该是拷贝构造吗?
    A test1()
    {
        A a(4);
        a.m_a = 5;
        cout<<"test1:"<<&a<<endl;
        return a;
    }

    int main()
    {
       test1();
        return 0;
    }
    展开

    作者回复: 你返回的是一个对象,prvalue,不是左值。这个对象会被移动返回。可以参见第 10 讲。

    如果你把返回值改成左值引用,返回的才是左值。在这种情况下,随机的崩溃就可能会发生了,因为被引用的对象已经被销毁。这也是为什么必须返回对象而不是引用,这样的对象会从返回内容移动或拷贝产生。

     1
    
我们在线,来聊聊吧