求教个 C++ Get 函数怎么写的问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Betsy
V2EX    C++

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

  •  
  •   Betsy 2024-08-08 00:18:44 +08:00 3531 次点击
    这是一个创建于 428 天前的主题,其中的信息可能已经有所发展或是发生改变。

    代码

    #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 种写法,为啥呢?这样有什么好处吗?

    37 条回复    2024-10-09 22:55:14 +08:00
    fgwmlhdkkkw
        1
    fgwmlhdkkkw  
       2024-08-08 00:27:46 +08:00   1
    c++的逻辑上“谁申请的谁释放”。
    sagaxu
        2
    sagaxu  
       2024-08-08 00:34:46 +08:00
    两个区别,一个是内存管理,一个是错误码。

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

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

    第一种写法更接近于 C 风格,返回值用于确定是否有错误,传入一个参数用于接收真正的返回值。很多公司会自己规定使用哪种写法,比如 Google 内部 C++规范实现了一个 StatusOr<T>的类型可以用一个返回值同时表示是否错误和实际返回值。
    tyzandhr
        5
    tyzandhr  
       2024-08-08 02:17:06 +08:00 via Android
    要是 std23 ,可以用 expected<Student>
    fgwmlhdkkkw
        6
    strong>fgwmlhdkkkw  
       2024-08-08 02:17:47 +08:00 via Android
    @fgwmlhdkkkw 我没仔细看代码,第二种写法遇到不存在的 key 就完蛋了呀
    fgwmlhdkkkw
        7
    fgwmlhdkkkw  
       2024-08-08 02:19:18 +08:00 via Android
    不是,第一种也不对呀
    iceheart
        8
    iceheart  
       2024-08-08 07:10:45 +08:00 via Android
    Get 语义不太清晰,个人喜欢 fetch
    bool fetch(const string &, valuetype &);
    或者:
    valuetype *fetch(const string &);
    henix
        9
    henix  
       2024-08-08 07:32:30 +08:00   2
    首先,这两种写法语义上并不等价,第一种写法多出一个 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  
       2024-08-08 09:32:53 +08:00 via Android
    第二种有临时变量效率不高,如果用引用,又有失效问题,当这个类释放后,get 传递的 引用变无效了
    Betsy
        11
    Betsy  
    OP
       2024-08-08 09:50:15 +08:00 via iPhone
    @sagaxu 这个如何复用?
    Betsy
        12
    Betsy  
    OP
       2024-08-08 09:50:37 +08:00 via iPhone
    @tool2dx 对,我也偏好第二种。
    Betsy
        13
    Betsy  
    OP
       2024-08-08 09:51:38 +08:00 via iPhone
    @tyzandhr 不需要这么高端吧,普世意义上的 C++
    Betsy
        14
    Betsy  
    OP
       2024-08-08 09:53:28 +08:00 via iPhone
    @fgwmlhdkkkw 第一种哪里不对?除过没判断 key 值是否存在导致潜在的 exception 之外。
    Betsy
        15
    Betsy  
    OP
       2024-08-08 10:01:26 +08:00 via iPhone
    @wnpllrzodiac
    1. 不是有 RVO 嘛,两个执行效率差不多的吧。
    2. 好像是会有这么个问题
    MoYi123
        16
    MoYi123  
       2024-08-08 10:32:06 +08:00   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  
       2024-08-08 10:35:49 +08:00
    写了一堆又删掉了,再次看到这个帖子还是忍不住想说点什么
    以下仅个人观点
    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  
       2024-08-08 10:39:58 +08:00
    都用指针不是效率更高吗。

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

    Student* Get(const std::string& key) { return this->map_[key]; }
    lovelylain
        19
    lovelylain  
       2024-08-08 10:52:15 +08:00
    this->map_[key] 当 key 不存在时会自动插入并返回,修改了 map 不符合 Get 语义,改为
    const Student* Get(const std::string& key) const;
    存在返回 value 地址,不存在返回 nullptr:
    1. 避免修改 map
    2. 避免拷贝
    GeruzoniAnsasu
        20
    GeruzoniAnsasu  
       2024-08-08 11:16:51 +08:00   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  
       2024-08-08 11:35:10 +08:00
    楼主上一个帖子里面也出现了类似的写法

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

    这句话是错的。你应该

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

    因为你真的返回的是临时对象啊,这句话执行完就没有了啊(
    ipwx
        22
    ipwx  
       2024-08-08 11:38:13 +08:00   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  
       2024-08-08 11:40:59 +08:00
    这样,楼主你把 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  
       2024-08-08 14:12:13 +08:00
    本质不就是错误处理,用写法二只能抛异常
    Betsy
        25
    Betsy  
    OP
       2024-08-08 22:24:42 +08:00
    @nevermoreluo 所以,你是建议使用 this->map_.at(key) 这样的写法吗?
    Betsy
        26
    Betsy  
    OP
       2024-08-08 22:27:31 +08:00
    @jones2000 我肯定是希望 class Table 释放的时候,map_ 中的 Student 也被释放的。如果按照你这种写法的话,首先我需要写一个析构函数,其次我需要在析构函数里面写 delete Student 的逻辑,感觉变得更加复杂了。
    Betsy
        27
    Betsy  
    OP
       2024-08-08 22:31:27 +08:00
    @lovelylain 这的确也是一种方法,但是最前面这个 const 会不会限制不住。

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

    const Student* p = Get("key");
    Student* q = const_cast<Student*>(p);
    q.name = "ahahah";
    Betsy
        28
    Betsy  
    OP
       2024-08-08 22:33:15 +08:00
    @ipwx 返回指针,会不会存在跟 #27 一样的问题?
    Betsy
        29
    Betsy  
    OP
       2024-08-08 22:34:48 +08:00
    @PTLin 抛异常我觉得也可,在 Java 中会有大量抛异常然后处理异常的逻辑。但是在 C++ 的项目中,好像不怎么用异常处理。
    jones2000
        30
    jones2000  
       2024-08-08 23:20:37 +08:00
    @Betsy c++玩的就是指针,否则和 java,py 有什么区别。可以做内存池统一分配回收。 闲麻烦外面套一个智能指针 shared_ptr 。
    Betsy
        31
    Betsy  
    OP
       2024-08-08 23:41:11 +08:00 via iPhone
    @jones2000 但也不希望把 C++ 写成 C 呀。高级特性还是要用用的
    lovelylain
        32
    lovelylain  
       2024-08-09 05:39:55 +08:00 via Android   1
    @ipwx 返回临时对象,用 const&接收返回值,符合 c++标准,没问题的,编译器会处理声明周期。
    @Betsy #25 不清楚就多查文档,[]访问在不存在的时候会自动插入,at 不会自动插入,因为要返回引用,所以不存在就只能抛异常了,要想不自动插入不抛异常,就最好返回 const*。
    @Betsy #27 代码是人写的,返回 const*表明了调用方不应该修改该对象,如果你非要较真可以去掉 const 后修改,那通过 table 地址还能获取到其私有 mp 地址呢,代码 xjb 写出啥问题都有可能。如果你需要修改,可以明确定义一个返回无 const 的版本。
    ipwx
        33
    ipwx  
       2024-08-09 12:02:41 +08:00   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  
       2024-08-09 12:06:55 +08:00
    @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  
       2024-08-09 12:39:04 +08:00 via Android
    @ipwx 你可真是个嘴强王者,22 楼的回复暴露了你对 C++的一个错误认识,甭管这种用法是否值得推荐,它确实是可以的,而非你理解的是错误用法。不用回复我了,你嘴强你高兴就好。
    ipwx
        36
    ipwx  
       2024-08-09 14:38:32 +08:00
    @lovelylain 你确实 C++ 的细节比我掌握的好,你是个人才。

    我只是不推荐楼主去陷入这种不适合工程开发的实践而已。
    Hackerl
        37
    Hackerl  
       2024-10-09 22:55:14 +08:00
    std::optional<std::reference_wrapper<const Student>> Get(const std::string& key);
    div style="box-sizing: border-box">
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1148 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 23:27 PVG 07:27 LAX 16:27 JFK 19:27
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86