开发者

C++在同一对象中存储左值或右值的方法

目录
  • 一、背景
  • 二、跟踪值
    • 2.1、存储引用
    • 2.2、存储值
  • 三、存储variant
    • 四、通用存储类
      • 4.1、定义const访问
      • 4.2、定义非const访问
    • 五、创建存储
      • 六、总结

        一、背景

        C++ 代码似乎经常出现一个问题:如果该值可以来自左值或右值,则对象如何跟踪该值?即如果保留该值作为引用,那么就无法绑定到临时对象。如果将其保留为一个值,那么当它从左值初始化时,会产生不必要的副本。

        有几种方法可以应对这种情况。使用std::variant提供了一个很好的折衷方案来获得有表现力的代码。

        二、跟踪值

        假设有一个类MyClass。想让MyClass访问某个std::string。如何表示MyClass内部的字符串?

        有两种选择:

        • 将其存储为引用。
        • 将其存储为副本。

        2.1、存储引用

        如果将其存储为引用,例如const引用:

        class MyClass
        {
        public:
            explicit MyClass(std::string const& s) : s_(s) {}
            void print() const
            {
                std::cout << s_ << '\n';
            }
        编程客栈private:
            std::string const& s_;
        };
        

        则可以用一个左值初始化我们的引用:

        std::string s = "hello";
        MyClass myObject{s};
        myObject.print();
        

        看起来很不错。但是,如果想用右值初始化我们的对象呢?例如:

        MyClass myObject{std::string{"hello"}};
        myObject.print();
        

        或者这样的代码:

        std::string getString(); // function declaration returning by value
        
        MyClass myObject{getString()};
        myObject.print();
        

        那么代码具有未定义的行为。原因是,临时字符串对象在创建它的同一条语句中被销毁。当调用print时,字符串已经被破坏,使用它是非法的,并导致未定义的行为。

        为了说明这一点,如果将std::string替换为类型X,并且在X的析构函数打印日志:

        struct X
        {
            ~X() { std::cout << "X destroyed" << '\n';}
        };
        
        class MyClass
        {
        public:
            explicit MyClass(X const& x) : x_(x) {}
            void print() const
            {
                // using x_;
            }
        private:
            X const& x_;
        };
        

        在调用的地方也打印日志:

        MyClass myObject(X{});
        std::cout << "before print" << '\n';
        myObject.print();
        

        输出:

        X destroyed
        before print
        

        可以看到,在尝试使用之前,这个X已经被破坏了。

        完整示例:

        #include <IOStream>
        #include <string>
        
        struct X
        {
            ~X() { std::cout << "X destroyed" << '\n';}
        };
        
        class MyClass
        {
        public:
            explicit MyClass(X const& x) : x_(x) {}
            void print()
            {
                (void) x_; // using x_;
            }
        private:
            X const& x_;
        };
        
        int main()
        {
        	MyClass myObject(X{});
        	std::cout << "before print" << '\n';
        	myObject.print();
        }
        

        2.2、存储值

        另一种选择是存储一个值。这允许使用move语义将传入的临时值移动到存储值中:

        class MyClass
        {
        public:
            explicit MyClass(std::string s) : s_(std::move(s)) {}
            void print() const
            {
                std::cout << s_ << '\n';
            }
        private:
            std::string s_;
        };
        

        现在调用它:

        MyClass myObject{std::string{"hello"}};
        myObject.print();
        

        产生两次移动(一次构造s,一次构造s_),并且没有未定义的行为。实际上,即使临时对象被销毁,print也会使用类内部的实例。

        不幸的是,如果带着左值返回到第一个调用点:

        std::string s = "hello";
        MyClass myObject{s};
        myObject.print();
        

        那么就不再做两次移动了:做了一次复制(构造s)和一次移动(构造s_)。

        更重要的是,我们的目的是给MyClass访问字符串的权限,如果做一个拷贝,就有了一个不同于进来的实例。所以它们不会同步。

        对于临时对象来说,这不是问题,因为它无论如何都会被销毁,并且我们在之前将它移了进来,所以仍然可以访问字符串。但是通过复制,我们不再给MyClass访问传入字符串的权限。

        所以存储一个值也不是一个好的解决方案。

        三、存储varandroidiant

        存储引用不是一个好的解决方案,存储值也不是一个好的解决方案。我们想做的是,如果引用是从左值初始化的,则存储引用;如果引用是从右值初始化的,则存储引用。

        但是数据成员只能是一种类型:值或引用,对吗?

        但是,对于std::variant,它可以是任意一个。不过,如果尝试在一个变量中存储引用,就像这样:

        std::variant<std::string, std::string const&>
        

        将得到一个编译错误:

        variant must have no reference alternative
        

        为了达到我们的目的,需要将引用放在另一个类型中;即必须编写特定的代码来处理数据成员。如果为std::string编写这样的代码,则不能将其用于其他类型。

        在这一点上,最好以通用的方式编写代码。

        四、通用存储类

        存储需要是一个值或一个引用。既然现在是为通用目的编写这段代码,那么也可以允许非const引用。由于变量不能直接保存引用,那么可以将它们存储到包装器中:

        template<typename T>
        struct NonConstReference
        {
            T& value_;
            explicit NonConstReference(T& value) : value_(value){};
        };
        
        template<typename T>
        struct ConstReference
        {
            T const& value_;
            explicit ConstReference(T const& value) : value_(value){};
        };
        
        template<typename T>
        struct Value
        {
            T value_;
            explicit Value(T&& value) : value_(std::move(value)) {}
        };
        

        将存储定义为这两种情况之一:

        template<typename T>
        using Storage = std::variant<Value<T>, ConstReference<T>, NonConstReference<T>>;
        

        现在需要通过提供引用来访问变量的底层值。创建了两种类型的访问:一种是const,另一种是非const

        4.1、定义const访问

        要定义const访问,需要使变量内部的三种可能类型中的每一种都产生一个const引用。

        为了访问变量中的数据,将使用std::visit和规范的overload 模式,这可以在c++ 17中实现:

        template<typename... Functions>
        struct overload : Functions...
        {
            using Functions::operator()...;
            overload(Functions... functions) : Functions(functions)... {}
        };
        

        要获得const引用,只需为每种variant创建一个:

        template<typename T>
        T const& getConstReference(Storage<T> const& storage)
        {
            return std::visit(
                overload(
                    [](Value<T> const& value) -> T const&             { return value.value_; }php,
                    [](NonConstReference<T> const& value) -> T const& { return value.value_; },
                    [](ConstReference<T> const& value) -> T const&    { return value.value_; }
                ),
                storage
            );
        }
        

        4.2、定义非const访问

        非const引用的创建使用相同的技术,除了variantConstReference之外,它不能产生非const引用。然而,当std::visit访问一个变量时,必须为它的每一个可能的类型编写代码:

        template<typename T>
        T& getReference(Storage<T>& storage)
        {
            return std::visit(
                overload(
                    [](Value<T>& value) -> T&             { return value.value_; },
                    [](NonConstReference<T>& value) -> T& { return value.value_; },
                    [](ConstReference<T>& ) -> T&.        { /* code handling the error! */ }
                ),
                storage
            );
        }
        

        进一步优化,抛出一个异常:

        struct NonConstReferenceFromReference : public std::runtime_erro编程客栈r
        {
            explicit NonConstReferenceFromReference(std::string const& what) : std::runtime_error{what} {}
        };
        
        template<typename T>
        T& getReference(Storage<T>& storage)
        {
            return std::visit(
                overload(
                    [](Value<T>& value) -> T&             { return value.value_; },
                    [](NonConstReference<T>& value) -> T& { return value.value_; },
                    [](ConstReference<T>& ) -> T& { throw NonConstReferenceFromReference{"Cannot get a non const reference from a const reference"} ; }
                ),
                storage
            );
        }
        

        五、创建存储

        已经定义了存储类,可以在示例中使用它来访问传入的std::string,而不管它的值类别:

        class MyClass
        {
        public:
            explicit MyClass(std::string& value) :       storage_(NonConstReference(value)){}
            explicit MyClass(std::string const& value) : storage_(ConstReference(value)){}
            explicit MyClass(std::string&& value) :      storage_(Value(std::move(value))){}
        
            void print() const
            {
                std::cout << getConstReference(storage_) << '\n';
            }
        
        private:
            Storage<std::string> storage_;
        };
        

        (1)调用时带左值:

        std::string s = "hello";
        MyClass myObject{s};
        myObject.print();
        

        匹配第一个构造函数,并在存储成员内部创建一个NonConstReference。当print函数调用getConstReference时,非const引用被转换为const引用。

        (2)使用临时值:

        MyClass myObject{std::string{"hello"}};
        myObject.print()编程客栈;
        

        这个函数匹配第三个构造函数,并将值移动到存储中。getConstReference然后将该值的const引用返回给print函数。

        六、总结

        variant为c++中跟踪左值或右值的经典问题提供了一种非常适合的解决方案。这种技术的代码具有表现力,因为std::variant允许表达与我们的意图非常接近的东西:“根据上下文,对象可以是引用或值”。

        在C++ 17和std::variant之前,解决这个问题很棘手,导致代码难以正确编写。随着语言的发展,标准库变得越来越强大,可以用越来越多的表达性代码来表达我们的意图。

        以上就是C++在同一对象中存储左值或右值的方法的详细内容,更多关于C++同一对象存储左值的资料请关注编程客栈(www.devze.com)其它相关文章!

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜