MirrorYuChen
MirrorYuChen
Published on 2025-01-27 / 66 Visits
0
0

C++中 `()`和 `{}`对象初始化方法区别

C++中 (){}对象初始化方法区别

​ 事情起因是这样的,在使用 pImpl封装接口时,遇到了一些问题,示意代码如下:

/*
 * @Description: Interface
 * @Author: chenjingyu
 * @Date: 2025-01-27 11:17:17
 * @FilePath: Interface.h
 */
#pragma once

#include <iostream>
#include <string>
#include <memory>

class Impl;
class Interface {
public:
  Interface();
  ~Interface();

  void Hello(const std::string &name);

private:
  std::unique_ptr<Impl> impl_ = nullptr;
};
/*
 * @Description: Interface
 * @Author: chenjingyu
 * @Date: 2025-01-27 11:18:57
 * @FilePath: Interface.cc
 */
#include "Interface.h"

class Impl {
public:
  Impl() = default;
  ~Impl() = default;
  void Hello(const std::string &name) {
    std::cout << "Hello, " << name << "!" << std::endl;
  }
};

Interface::Interface() : impl_(new Impl()) {}

Interface::~Interface() = default;

void Interface::Hello(const std::string &name) {
  impl_->Hello(name);
}
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include "Interface.h"

int main(int argc, char *argv[]) {
  Interface interface;
  interface.Hello("World");
  return 0;
}

​ 编译时,使用 MSVC不会报错,但是使用 MinGW和GCC会报如下错误:

[1/3] Building CXX object CMakeFiles/test.dir/main.cc.obj
FAILED: CMakeFiles/test.dir/main.cc.obj 
"D:\software\JetBrains\CLion 2023.2.2\bin\mingw\bin\g++.exe"  -isystem D:/library/vcpkg/installed/x64-windows/include/opencv3 -isystem D:/library/vcpkg/installed/x64-windows/include/opencv3/opencv -g -std=gnu++11 -fdiagnostics-color=always -MD -MT CMakeFiles/test.dir/main.cc.obj -MF CMakeFiles\test.dir\main.cc.obj.d -o CMakeFiles/test.dir/main.cc.obj -c G:/project/test/main.cc
In file included from D:/software/JetBrains/CLion 2023.2.2/bin/mingw/lib/gcc/x86_64-w64-mingw32/13.1.0/include/c++/memory:78,
                 from G:/project/test/Interface.h:11,
                 from G:/project/test/main.cc:7:
D:/software/JetBrains/CLion 2023.2.2/bin/mingw/lib/gcc/x86_64-w64-mingw32/13.1.0/include/c++/bits/unique_ptr.h: In instantiation of 'void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Impl]':
D:/software/JetBrains/CLion 2023.2.2/bin/mingw/lib/gcc/x86_64-w64-mingw32/13.1.0/include/c++/bits/unique_ptr.h:404:17:   required from 'std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Impl; _Dp = std::default_delete<Impl>]'
G:/project/test/Interface.h:22:33:   required from here
D:/software/JetBrains/CLion 2023.2.2/bin/mingw/lib/gcc/x86_64-w64-mingw32/13.1.0/include/c++/bits/unique_ptr.h:97:23: error: invalid application of 'sizeof' to incomplete type 'Impl'
   97 |         static_assert(sizeof(_Tp)>0,
      |                       ^~~~~~~~~~~
[2/3] Building CXX object CMakeFiles/test.dir/Interface.cc.obj
ninja: build stopped: subcommand failed.

​ 这个时候,你只需要将 Interface.h中的 std::unique_ptr<Impl> impl_ = nullptr;改为 std::unique_ptr<Impl> impl_ {nullptr};即可正常编译通过。那么问题出在那里呢?首先,我们来看一下常见的几种对象初始化?

1.常见几种对象初始化

1.1 赋值初始化

/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>

class A {
public:
  A(int x) {
    std::cout << "A constructor" << std::endl;
  }

  ~A() {
    std::cout << "A destructor" << std::endl;
  }

  A(const A&) {
    std::cout << "A copy constructor" << std::endl;
  }

  A &operator=(const A&) {
    std::cout << "A copy assignment operator" << std::endl;
    return *this;
  }
};

int main(int argc, char *argv[]) {
  A a = 10;
  return 0;
}

​ 编译时,添加选项 -fno-elide-constructors,禁用掉构造函数的返回值优化(Return Value Optimization, RVO)。默认情况下,GCC和Clang编译器会对返回值进行优化,省略掉创建一个只为初始化另一个同类型对象的临时对象。

​ 编译运行有:

>> g++ -fno-elide-constructors -o main main.cc
>> ./main
A constructor
A copy constructor
A destructor
A destructor

​ 可以看到,A a = 10;这里会调用一次构造函数和一次拷贝构造函数。背后实现过程如下:

  • (1) 隐式构造一个A的实例:A(10);
  • (2) 拷贝构造方式初始化实例a:A a = A(10);

​ A a = 10;等价于 A a = A(10);,当然,A a = 10;可能会造成语义不清晰的问题,一个 A类型 a怎么等于一个数10,为了解决这个问题,C++引入了关键字 explicit,禁用掉这种隐式转换,避免引入不必要错误。

/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>

class A {
public:
  explicit A(int x) {
    std::cout << "A constructor" << std::endl;
  }

  ~A() {
    std::cout << "A destructor" << std::endl;
  }

  A(const A&) {
    std::cout << "A copy constructor" << std::endl;
  }

  A &operator=(const A&) {
    std::cout << "A copy assignment operator" << std::endl;
    return *this;
  }
};

int main(int argc, char *argv[]) {
  A a = A(10);
  return 0;
}

​ 这时,你用 A a = 10;编译器就会报错了。文章开始时提到的 std::unique_ptr<Impl> impl_ = nullptr;代码,会先调用 std::unique_ptr的构造函数构造一个实例,然后调用移动构造(禁用了拷贝构造)函数来初始化 impl_

unique_ptr(unique_ptr&& __u) noexcept
  : _M_t(__u.release(), std::forward<deleter_type>(__u.get_deleter())) {}

​ 移动构造函数中会先调用 release()接口将资源所有权转移给当前对象,而 release 方法的实现依赖于指针传入类型 _Tp 的完整类型信息,因此会报错。

1.2 圆括号初始化

/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>

class A {
public:
  explicit A(int x) {
    std::cout << "A constructor" << std::endl;
  }

  ~A() {
    std::cout << "A destructor" << std::endl;
  }

  A(const A&) {
    std::cout << "A copy constructor" << std::endl;
  }

  A &operator=(const A&) {
    std::cout << "A copy assignment operator" << std::endl;
    return *this;
  }
};

int main(int argc, char *argv[]) {
  A a(10);
  return 0;
}

​ 编译运行:

>> g++ -fno-elide-constructors -o main main.cc
>> ./main                         
A constructor
A destructor

​ 可以看到,只调用了一次构造函数,结果符合预期。但是,我们换一种方式调用看看:

/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>

class A {
public:
  explicit A(int x) {
    std::cout << "A constructor" << std::endl;
  }

  ~A() {
    std::cout << "A destructor" << std::endl;
  }

  A(const A&) {
    std::cout << "A copy constructor" << std::endl;
  }

  A &operator=(const A&) {
    std::cout << "A copy assignment operator" << std::endl;
    return *this;
  }
};


int main(int argc, char *argv[]) {
  float value = 10.0f;
  A a(int(value));

  return 0;
}

​ 再次编译运行:

>> g++ -fno-elide-constructors -o main main.cc
>> ./main

​ 可以发现,什么都没打印,因为这里 A a(int(value));会被编译器解析成函数声明 A a(int value);,这也就是我们常说的 most vexing parse(令人困惑的解析)。

​ **Most Vexing Parse**是C++中一个常见的语法陷阱,它涉及C++解析规则,导致某些代码含义与程序员意图不符,这个问题常出现在使用圆括号初始化对象时,编译器可能会将代码解析成函数声明,而不是对象定义。常见解决办法就是使用列表初始化。

1.3 列表初始化

​ 列表初始化就是使用花括号来进行初始化,例如:

/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>

class A {
public:
  explicit A(int x) {
    std::cout << "A constructor" << std::endl;
  }

  ~A() {
    std::cout << "A destructor" << std::endl;
  }

  A(const A&) {
    std::cout << "A copy constructor" << std::endl;
  }

  A &operator=(const A&) {
    std::cout << "A copy assignment operator" << std::endl;
    return *this;
  }
};

int main(int argc, char *argv[]) {
  float value = 10.0f;
  A a{int(value)};
  return 0;
}

​ 编译运行:

>> g++ -fno-elide-constructors -o main main.cc
>> ./main
A constructor
A destructor

2.列表初始化

​ 使用列表初始化可以避免赋值初始化和圆括号初始化所带来的不良因素,那么使用列表初始化到底解决了哪些问题呢?

  • (1) 完美解决Most Vexing Parse问题,并避免窄化转换:列表初始化会避免窄化转换(narrowing conversion),即从一个较大的类型转换为一个较小的类型。
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>

class A {
public:
  A(int x, float y) {
    std::cout << "A constructor" << std::endl;
  }

  ~A() {
    std::cout << "A destructor" << std::endl;
  }
};

int main(int argc, char *argv[]) {
  float value = 10.0f;
  A a{10, 11.0f};
  // A b{11.0f, 10};    // 错误,不允许实际传参类型与定义不一致
  return 0;
}

  • (2) 大大简化聚合类初始化:它提供了一种统一的初始化语法,使代码更加简洁和一致。
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>

struct Point {
  float x;
  float y;
};

struct Rectangle {
  int width;
  int height;
  Point topLeft;
  Point bottomRight;
};

int main(int argc, char *argv[]) {
  Point pt {1.0f, 2.0f};
  Rectangle rect{100, 200, {0, 0}, {100, 200}};

  std::cout << "Rectangle: width=" << rect.width
            << ", height=" << rect.height
            << ", topLeft=(" << rect.topLeft.x << ", " << rect.topLeft.y
            << "), bottomRight=(" << rect.bottomRight.x << ", " << rect.bottomRight.y << ")\n";
  return 0;
}

​ 编译运行:

>> g++ -fno-elide-constructors -o main main.cc
>> ./main
Rectangle: width=100, height=200, topLeft=(0, 0), bottomRight=(100, 200)

3.std::initializer_list

3.1 基本用法介绍

​ std::initializer_list 是 C++11 引入的一个模板类,用于表示初始化列表。初始化列表是由花括号 {} 包围的一系列值,通常用于初始化数组、容器或类的成员。

​ std::initializer_list 提供了一种类型安全的方式来处理这些初始化列表。

  • size():返回初始化列表中的元素数量。
  • begin():返回指向初始化列表中第一个元素的指针。
  • end():返回指向初始化列表中最后一个元素之后的指针,需要访问最后一个元素需要使用 end() - 1
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>
#include <initializer_list>

class myClass {
public:
  myClass(std::initializer_list<int> list) {
    auto size = list.size();
    std::cout << "size: " << size << std::endl;
    std::cout << "first element: " << *list.begin() << std::endl;
    std::cout << "last element: " << *(list.end() - 1) << std::endl;
    for (auto elem: list) {
      std::cout << elem << " ";
    }
    std::cout << std::endl;
  }
};

int main(int argc, char *argv[]) {
  myClass obj = {1, 2, 3, 4, 5};
  return 0;
}

​ 编译运行:

>> g++ -fno-elide-constructors -o main main.cc
>> ./main
size: 5
first element: 1
last element: 5
1 2 3 4 5 

3.2 使用注意事项

  • (1) 除非不得已,即使报错,也会优先匹配 std::initializer_list
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>
#include <initializer_list>

class myClass {
public:
  myClass(std::initializer_list<int> list) {
    auto size = list.size();
    std::cout << "size: " << size << std::endl;
    std::cout << "first element: " << *list.begin() << std::endl;
    std::cout << "last element: " << *(list.end() - 1) << std::endl;
    for (auto elem: list) {
      std::cout << elem << " ";
    }
    std::cout << std::endl;
  }

  myClass(int x, int y) {
    std::cout << "myClass(int, int)" << std::endl;
  }

  myClass(int x, float y) {
    std::cout << "myClass(int, float)" << std::endl;
  }
  
  myClass(int x, const std::string &y) {
    std::cout << "myClass(int, const std::string&)" << std::endl;
  }
};

int main(int argc, char *argv[]) {
  {
    myClass obj = {1, 2};
  }
  {
    // 报错,即使报错,也会优先比配std::initializer_list
    // 这里2.0f可以隐式转换成int类型
    // myClass obj = {1, 2.0f};  
  }
  {
    // 万不得已
    // 不存在const char *转int的隐式转换
    myClass obj = {1, "Hello"};
  }
  return 0;
}

​ 编译运行:

>> g++ -fno-elide-constructors -o main main.cc
>> ./main
size: 2
first element: 1
last element: 2
1 2 
myClass(int, const std::string&)
  • (2)空{}不会调用 std::initializer_list
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>
#include <initializer_list>

class myClass {
public:
  myClass() {
    std::cout << "construct." << std::endl;
  }

  myClass(std::initializer_list<int> list) {
    auto size = list.size();
    std::cout << "size: " << size << std::endl;
    std::cout << "first element: " << *list.begin() << std::endl;
    std::cout << "last element: " << *(list.end() - 1) << std::endl;
    for (auto elem: list) {
      std::cout << elem << " ";
    }
    std::cout << std::endl;
  }

  myClass(int x, int y) {
    std::cout << "myClass(int, int)" << std::endl;
  }

  myClass(int x, float y) {
    std::cout << "myClass(int, float)" << std::endl;
  }
};

int main(int argc, char *argv[]) {
  {
    myClass obj {};
  }
  return 0;
}

​ 编译运行:

>> g++ -fno-elide-constructors -o main main.cc
>> ./main
construct.

4.参考资料


Comment