V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Betsy
V2EX  ›  C++

求教个 C++ Get 函数怎么写的问题

  •  
  •   Betsy · 40 天前 · 2219 次点击
    这是一个创建于 40 天前的主题,其中的信息可能已经有所发展或是发生改变。

    代码

    #include<iostream>
    #include <cstdint>
    #include <unordered_map>
    
    enum struct Status {
        kOk = 0,
    };
    
    struct Student {
        std::string name;
        std::size_t age;
    };
    
    class Table {
       public:
        Table() {
            this->map_.insert(std::make_pair("w1", Student("li", 23)));
            this->map_.insert(std::make_pair("s2", Student("zhao", 18)));
        }
    
        Status Get(const std::string& key, Student* value) {
            *value = this->map_[key];
            return Status::kOk;
        }
    
        Student Get(const std::string& key) { return this->map_[key]; }
    
       private:
        std::unordered_map<std::string, Student> map_;
    };
    
    int main(int argc, char* argv[]) {
        Table table;
    
        Student stu1;
        const Status& status = table.Get("w1", &stu1);
        std::cout << stu1.name << ":" << stu1.age << std::endl;
    
        const Student& stu2 = table.Get("s2");
        std::cout << stu2.name << ":" << stu2.age << std::endl;
        return 0;
    }
    

    结果

    li:23
    zhao:18
    

    问题

    1. Status Get(const std::string& key, Student* value);
    2. Student Get(const std::string& key);
    

    在 Java/Python 等语言中,个人更喜欢第 2 种写法;但是 C++ 中,一些项目更倾向于第 1 种写法,为啥呢?这样有什么好处吗?

    36 条回复    2024-08-09 14:38:32 +08:00
    fgwmlhdkkkw
        1
    fgwmlhdkkkw  
       40 天前   ❤️ 1
    c++的逻辑上“谁申请的谁释放”。
    sagaxu
        2
    sagaxu  
       40 天前
    两个区别,一个是内存管理,一个是错误码。

    Java/Python 没有那么细致的内存管理,比如 C++中,可以栈上分配 Student ,并且复用
    tool2dx
        3
    tool2dx  
       40 天前
    我也喜欢第二种写法。第一种写法是偏向编译效率,以前 C++还没有&&和 move ,所以会多一次临时拷贝。

    但其实不写游戏,不考虑效率,第二种更便于理解。
    fpk5
        4
    fpk5  
       40 天前   ❤️ 1
    C++有一个问题就是怎么区分错误和正常返回,不像 Java 和 Python ,C++的 exception 不一定 work as expected 。你的例子里的 unordered_map::operator[]是有可能失败的(比如内存不足),你的 caller 不一定能正确处理这种 exception 。

    第一种写法更接近于 C 风格,返回值用于确定是否有错误,传入一个参数用于接收真正的返回值。很多公司会自己规定使用哪种写法,比如 Google 内部 C++规范实现了一个 StatusOr<T>的类型可以用一个返回值同时表示是否错误和实际返回值。
    tyzandhr
        5
    tyzandhr  
       40 天前 via Android
    要是 std23 ,可以用 expected<Student>
    fgwmlhdkkkw
        6
    fgwmlhdkkkw  
       40 天前 via Android
    @fgwmlhdkkkw 我没仔细看代码🤪,第二种写法遇到不存在的 key 就完蛋了呀
    fgwmlhdkkkw
        7
    fgwmlhdkkkw  
       40 天前 via Android
    不是,第一种也不对呀🫵
    iceheart
        8
    iceheart  
       39 天前 via Android
    Get 语义不太清晰,个人喜欢 fetch
    bool fetch(const string &, valuetype &);
    或者:
    valuetype *fetch(const string &);
    henix
        9
    henix  
       39 天前   ❤️ 1
    首先,这两种写法语义上并不等价,第一种写法多出一个 Status ,第二种写法要加上 Status 的话得返回一个 std::tuple<Status, Student> 或 std::variant<Status, Student>

    两者的区别在于,第一种写法,Student 占用的内存由调用方分配,适用于对性能要求较高的场景;第二种写法,每调用一次 Get ,都会为返回的 Student 分配内存(尤其是 Student 包含了一个 string ,string 是动态分配),好处是用起来更方便。

    考虑在一个循环中调用 Get ,如果用第一种写法,可以在循环外初始化 Student 并且复用 Student ,从而减少内存分配次数:

    Student stu;
    for (...) {
    Get(key, &stu);
    }
    wnpllrzodiac
        10
    wnpllrzodiac  
       39 天前 via Android
    第二种有临时变量效率不高,如果用引用,又有失效问题,当这个类释放后,get 传递的 引用变无效了
    Betsy
        11
    Betsy  
    OP
       39 天前 via iPhone
    @sagaxu 这个如何复用?
    Betsy
        12
    Betsy  
    OP
       39 天前 via iPhone
    @tool2dx 对,我也偏好第二种。
    Betsy
        13
    Betsy  
    OP
       39 天前 via iPhone
    @tyzandhr 不需要这么高端吧,普世意义上的 C++
    Betsy
        14
    Betsy  
    OP
       39 天前 via iPhone
    @fgwmlhdkkkw 第一种哪里不对?除过没判断 key 值是否存在导致潜在的 exception 之外。
    Betsy
        15
    Betsy  
    OP
       39 天前 via iPhone
    @wnpllrzodiac
    1. 不是有 RVO 嘛,两个执行效率差不多的吧。
    2. 好像是会有这么个问题
    MoYi123
        16
    MoYi123  
       39 天前   ❤️ 1
    一般用
    const Student& Get(const std::string& key) const { return this->map_.at(key); }
    这样拷贝构造发生在外部.

    如果有需要再加上 Student& Get(const std::string& key) const { return this->map_.at(key); }
    可以支持 move

    Status Get(const std::string& key, Student* value); 这种写法是 C 语言的风格. 不建议用
    nevermoreluo
        17
    nevermoreluo  
       39 天前
    写了一堆又删掉了,再次看到这个帖子还是忍不住想说点什么
    以下仅个人观点
    Student Get(const std::string& key) { return this->map_[key]; } 抛开内存效率不谈这个接口都不好

    我最开始也觉得为什么不跟 py 一样直接返回对象呢,其实是因为 map_[key]这个用法和异常处理不一样。
    map_[key]这个操作会在 key 不存在时构造一个,而 py 会返回 KeyError 。
    那么既然报错了你就要处理,所以 py 这里的 KeyError 的异常其实隐式表达了 Status 中 NotFound 的概念。
    另外我个人觉得这个不存在时构造一个是个定时炸弹,不要在拉屎后盖上沙子,否则可能要在某个午后一堆人找屎
    jones2000
        18
    jones2000  
       39 天前
    都用指针不是效率更高吗。

    std::unordered_map<std::string, Student*> map_;

    Student* Get(const std::string& key) { return this->map_[key]; }
    lovelylain
        19
    lovelylain  
       39 天前
    this->map_[key] 当 key 不存在时会自动插入并返回,修改了 map 不符合 Get 语义,改为
    const Student* Get(const std::string& key) const;
    存在返回 value 地址,不存在返回 nullptr:
    1. 避免修改 map
    2. 避免拷贝
    GeruzoniAnsasu
        20
    GeruzoniAnsasu  
       39 天前   ❤️ 1
    orz

    完美体现 c++有多复杂的例子。
    可以去考虑的点:

    - key 用 string 接收还是 string_view 接收? 后者支持从一段 parse 后的文本中提取一段作为 key
    - student 返回时要不要创建单独的生命周期?如何保证/需不需要保证返回的 student 引用(指针)一定有效
    - 异常处理范式用什么? 是错误码还是 optional 还是 expected 还是抛异常
    - get 接口适不适合定义为 const ? 如果 const 的话返回的对象将不可修改,如果要进行二次处理则会引入额外复制,如果不 const 的话会存在非预期地修改了原 map 的隐患,破坏 get 的语义
    - 多种 get 方式适不适合作为重载实现,还是重命名成不同的 get_xxx 比较好
    ipwx
        21
    ipwx  
       39 天前
    楼主上一个帖子里面也出现了类似的写法

    const Status& status = table.Get("w1", &stu1);

    这句话是错的。你应该

    Status status = table.Get("w1", &stu1);

    因为你真的返回的是临时对象啊,这句话执行完就没有了啊(
    ipwx
        22
    ipwx  
       39 天前   ❤️ 1
    额其实第二句也是错的

    const Student& stu2 = table.Get("s2");

    它只能是

    Student stu2 = table.Get("s2");

    因为你在类里面

    Student Get(const std::string& key) { return this->map_[key]; }

    它返回的是 this->map_[key]; 的一个拷贝,而不是 this->map_[key]; 它本身的引用。
    ====


    如果你要写成

    const Student& stu2 = table.Get("s2");

    你对应的类里面应该写成

    const Student& Get(const std::string& key) { return this->map_[key]; }
    ===

    楼主对于 C++ 对象的生存周期是完全不理解啊。。。
    ipwx
        23
    ipwx  
       39 天前
    这样,楼主你把 C++ 的引用看成 “指针” 的语法糖就行了。

    引用基本就是指针。。。

    =====

    所以你的第一种,一般可以写成(没有过编译器,手写,不保真):


    const Student* Get(const std::string& key) {
    auto it = this->map_.find(key);
    return (it != this->map_.end()) ? &(it.second) : std::nullptr;
    }


    然后用的时候

    auto myStudent = table.Get("w1");
    if (myStudent) {
    ...
    }
    PTLin
        24
    PTLin  
       39 天前
    本质不就是错误处理,用写法二只能抛异常
    Betsy
        25
    Betsy  
    OP
       39 天前
    @nevermoreluo 所以,你是建议使用 this->map_.at(key) 这样的写法吗?
    Betsy
        26
    Betsy  
    OP
       39 天前
    @jones2000 我肯定是希望 class Table 释放的时候,map_ 中的 Student 也被释放的。如果按照你这种写法的话,首先我需要写一个析构函数,其次我需要在析构函数里面写 delete Student 的逻辑,感觉变得更加复杂了。
    Betsy
        27
    Betsy  
    OP
       39 天前
    @lovelylain 这的确也是一种方法,但是最前面这个 const 会不会限制不住。

    比如,在复杂逻辑下,会不会出现把 map 中的对象属性给修改掉的问题。

    const Student* p = Get("key");
    Student* q = const_cast<Student*>(p);
    q.name = "ahahah";
    Betsy
        28
    Betsy  
    OP
       39 天前
    @ipwx 返回指针,会不会存在跟 #27 一样的问题?
    Betsy
        29
    Betsy  
    OP
       39 天前
    @PTLin 抛异常我觉得也可,在 Java 中会有大量抛异常然后处理异常的逻辑。但是在 C++ 的项目中,好像不怎么用异常处理。
    jones2000
        30
    jones2000  
       39 天前
    @Betsy c++玩的就是指针,否则和 java,py 有什么区别。可以做内存池统一分配回收。 闲麻烦外面套一个智能指针 shared_ptr 。
    Betsy
        31
    Betsy  
    OP
       39 天前 via iPhone
    @jones2000 但也不希望把 C++ 写成 C 呀。高级特性还是要用用的
    lovelylain
        32
    lovelylain  
       38 天前 via Android   ❤️ 1
    @ipwx 返回临时对象,用 const&接收返回值,符合 c++标准,没问题的,编译器会处理声明周期。
    @Betsy #25 不清楚就多查文档,[]访问在不存在的时候会自动插入,at 不会自动插入,因为要返回引用,所以不存在就只能抛异常了,要想不自动插入不抛异常,就最好返回 const*。
    @Betsy #27 代码是人写的,返回 const*表明了调用方不应该修改该对象,如果你非要较真可以去掉 const 后修改,那通过 table 地址还能获取到其私有 map 地址呢,代码 xjb 写出啥问题都有可能。如果你需要修改,可以明确定义一个返回无 const 的版本。
    ipwx
        33
    ipwx  
       38 天前   ❤️ 1
    @lovelylain “ 返回临时对象,用 const&接收返回值,符合 c++标准,没问题的,编译器会处理声明周期。”

    嘿你可真是个人才,还真有这个规范:

    https://en.cppreference.com/w/cpp/language/reference_initialization

    Lifetime of a temporary

    Whenever a reference is bound to a temporary or to a subobject thereof, the lifetime of the temporary is extended to match the lifetime of the reference, with the following exceptions:

    a temporary bound to a return value of a function in a return statement is not extended: it is destroyed immediately at the end of the return expression. Such function always returns a dangling reference.
    a temporary bound to a reference member in a constructor initializer list persists only until the constructor exits, not as long as the object exists. (note: such initialization is ill-formed as of DR 1696).
    (until C++14)
    a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call: if the function returns a reference, which outlives the full expression, it becomes a dangling reference.
    a temporary bound to a reference in the initializer used in a new-expression exists until the end of the full expression containing that new-expression, not as long as the initialized object. If the initialized object outlives the full expression, its reference member becomes a dangling reference.
    a temporary bound to a reference in a reference element of an aggregate initialized using direct-initialization syntax (parentheses) as opposed to list-initialization syntax (braces) exists until the end of the full expression containing the initializer.

    可是这规范又臭又长,而且 "with the following exceptions" 一不留神就用错。为啥不用肯定没问题、而且编译器会负责优化的返回值拷贝呢?
    ipwx
        34
    ipwx  
       38 天前
    @lovelylain 简单来说,我反对任何形式地依赖这种语义的写法,原因如下:


    const A& a = F();


    这句话到底会不会产生一个 BUG ,依赖于 F() 的实现。如果 F() 不符合规范中的情形,你这种写法可能会出错。

    对于一个工程而言,如果不能在调用方确定上述用法对不对,那就是个灾难。比如

    template <typename F>
    void someFunction(F&& f) {
    const A& a = F();
    ...
    }

    当别人复用你的 someFunction 的时候,它就是个隐藏炸弹。

    ====


    @Betsy 27 楼的问题,人家想做的时候照样能够先把 const T& cast 到 const T* 然后 const cast 。。。

    想用是拦不住的。
    lovelylain
        35
    lovelylain  
       38 天前 via Android
    @ipwx 你可真是个嘴强王者,22 楼的回复暴露了你对 C++的一个错误认识,甭管这种用法是否值得推荐,它确实是可以的,而非你理解的是错误用法。不用回复我了,你嘴强你高兴就好。
    ipwx
        36
    ipwx  
       38 天前
    @lovelylain 你确实 C++ 的细节比我掌握的好,你是个人才。

    我只是不推荐楼主去陷入这种不适合工程开发的实践而已。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   934 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 19:31 · PVG 03:31 · LAX 12:31 · JFK 15:31
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.