Khởi tạo cấu trúc C ++ thuận tiện


141

Tôi đang cố gắng tìm một cách thuận tiện để khởi tạo các cấu trúc C 'pod'. Bây giờ, hãy xem xét các cấu trúc sau:

struct FooBar {
  int foo;
  float bar;
};
// just to make all examples work in C and C++:
typedef struct FooBar FooBar;

Nếu tôi muốn khởi tạo thuận tiện điều này trong C (!), Tôi có thể chỉ cần viết:

/* A */ FooBar fb = { .foo = 12, .bar = 3.4 }; // illegal C++, legal C

Lưu ý rằng tôi muốn tránh một cách rõ ràng các ký hiệu sau, bởi vì nó tấn công tôi như bị làm gãy cổ nếu tôi thay đổi bất cứ điều gì trong cấu trúc trong tương lai:

/* B */ FooBar fb = { 12, 3.4 }; // legal C++, legal C, bad style?

Để đạt được điều tương tự (hoặc ít nhất là tương tự) trong C ++ như trong /* A */ví dụ, tôi sẽ phải thực hiện một hàm tạo ngu ngốc:

FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) {}
// ->
/* C */ FooBar fb(12, 3.4);

Cái nào tốt cho nước sôi, nhưng không thích hợp cho người lười biếng (lười biếng là một điều tốt, phải không?). Ngoài ra, nó cũng khá tệ như /* B */ví dụ, vì nó không nói rõ giá trị nào thuộc về thành viên nào.

Vì vậy, câu hỏi của tôi về cơ bản là làm thế nào tôi có thể đạt được một cái gì đó tương tự như /* A */ hoặc tốt hơn trong C ++? Ngoài ra, tôi sẽ ổn với một lời giải thích tại sao tôi không muốn làm điều này (tức là tại sao mô hình tinh thần của tôi là xấu).

BIÊN TẬP

Bởi thuận tiện , tôi có nghĩa là cũng có thể duy trìkhông dư thừa .


2
Tôi nghĩ rằng ví dụ B gần như bạn sẽ nhận được.
Marlon

2
Tôi không thấy ví dụ B là "phong cách xấu". Nó có ý nghĩa với tôi, vì bạn đang khởi tạo lần lượt từng thành viên với các giá trị tương ứng của họ.
Mike Bailey

26
Mike, đó là phong cách xấu bởi vì không rõ giá trị nào thuộc về thành viên nào. Bạn phải đi và xem xét định nghĩa của struct và sau đó đếm các thành viên để tìm ra ý nghĩa của từng giá trị.
jnnnnn

9
Thêm vào đó, nếu định nghĩa về FooBar sẽ thay đổi trong tương lai, việc khởi tạo có thể bị phá vỡ.
Edward Falk

nếu việc khởi tạo trở nên dài và phức tạp, đừng quên mẫu xây dựng
kéo dài

Câu trả lời:


20

Các khởi tạo được chỉ định sẽ được hỗ trợ trong c ++ 2a, nhưng bạn không phải chờ đợi, vì chúng được hỗ trợ bởi GCC, Clang và MSVC.

#include <iostream>
#include <filesystem>

struct hello_world {
    const char* hello;
    const char* world;
};

int main () 
{
    hello_world hw = {
        .hello = "hello, ",
        .world = "world!"
    };

    std::cout << hw.hello << hw.world << std::endl;
    return 0;
}

GCC Demo MSVC Demo


Caveat emptor: hãy nhớ rằng nếu bạn thêm các tham số vào cuối cấu trúc sau này, các khởi tạo cũ sẽ vẫn âm thầm biên dịch mà không được khởi tạo.
Catskul

1
@Catskul Không. Nó sẽ được khởi tạo với danh sách khởi tạo trống, điều này sẽ dẫn đến khởi tạo với số không.
ivaigult

Bạn đúng. Cảm ơn bạn. Tôi nên làm rõ, các tham số còn lại sẽ âm thầm được khởi tạo mặc định một cách hiệu quả. Điểm tôi muốn nói là bất kỳ ai hy vọng điều này có thể giúp thực thi việc khởi tạo hoàn toàn các loại POD sẽ bị thất vọng.
Catskul

43

style AC ++ không được phép và bạn không muốn style Bsử dụng style BX:

FooBar fb = { /*.foo=*/ 12, /*.bar=*/ 3.4 };  // :)

Ít nhất là giúp đỡ ở một mức độ nào đó.


8
+1: nó không thực sự đảm bảo khởi tạo chính xác (từ trình biên dịch POV) nhưng chắc chắn sẽ giúp người đọc ... mặc dù các bình luận phải được giữ đồng bộ.
Matthieu M.

18
Nhận xét không ngăn việc khởi tạo cấu trúc bị phá vỡ nếu tôi chèn trường mới giữa foobartrong tương lai. C vẫn sẽ khởi tạo các trường mà chúng ta muốn, nhưng C ++ thì không. Và đây là điểm của câu hỏi - làm thế nào để đạt được kết quả tương tự trong C ++. Ý tôi là, Python thực hiện điều này với các đối số được đặt tên, C - với các trường "có tên" và C ++ cũng nên có một cái gì đó, tôi hy vọng.
dmitry_romanov

2
Nhận xét đồng bộ? Hãy cho tôi một break. An toàn đi qua cửa sổ. Sắp xếp lại các tham số và bùng nổ. Tốt hơn nhiều với explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) . Lưu ý từ khóa rõ ràng . Ngay cả việc phá vỡ các tiêu chuẩn là tốt hơn về an toàn. Trong Clang: -Wno-c99-phần mở rộng
Daniel O

@DanielW, Không phải về cái gì tốt hơn hay cái gì không. câu trả lời này phù hợp với việc OP không muốn Kiểu A (không phải c ++), B hoặc C, bao gồm tất cả các trường hợp hợp lệ.
iammilind

@iammilind Tôi nghĩ một gợi ý về lý do tại sao mô hình tinh thần của OP là xấu có thể cải thiện câu trả lời. Tôi coi điều này là nguy hiểm như bây giờ.
Daerst 10/2/2015

10

Bạn có thể sử dụng lambda:

const FooBar fb = [&] {
    FooBar fb;
    fb.foo = 12;
    fb.bar = 3.4;
    return fb;
}();

Thông tin thêm về thành ngữ này có thể được tìm thấy trên blog của Herb Sutter .


1
Cách tiếp cận như vậy khởi tạo các trường hai lần. Khi ở trong nhà xây dựng. Thứ hai là fb.XXX = YYY.
Dmytro Ovdiienko

9

Trích xuất các phần tử vào các hàm mô tả chúng (tái cấu trúc cơ bản):

FooBar fb = { foo(), bar() };

Tôi biết rằng kiểu đó rất gần với kiểu bạn không muốn sử dụng, nhưng nó cho phép thay thế dễ dàng hơn các giá trị không đổi và cũng giải thích chúng (do đó không cần chỉnh sửa nhận xét), nếu chúng thay đổi.

Một điều khác bạn có thể làm (vì bạn lười biếng) là đặt hàm tạo nội tuyến, vì vậy bạn không phải nhập nhiều (xóa "Foobar ::" và thời gian chuyển đổi giữa tệp h và cpp):

struct FooBar {
  FooBar(int f, float b) : foo(f), bar(b) {}
  int foo;
  float bar;
};

1
Tôi đặc biệt khuyên mọi người khác đọc câu hỏi này để chọn kiểu trong đoạn mã dưới cùng cho câu trả lời này nếu tất cả những gì bạn muốn làm là có thể nhanh chóng khởi tạo cấu trúc với một bộ giá trị.
kayleeFrye_onDeck

8

Câu hỏi của bạn hơi khó vì ngay cả chức năng:

static FooBar MakeFooBar(int foo, float bar);

có thể được gọi là:

FooBar fb = MakeFooBar(3.4, 5);

bởi vì các quy tắc quảng cáo và chuyển đổi cho các loại số tích hợp. (C chưa bao giờ được gõ mạnh)

Trong C ++, những gì bạn muốn là có thể đạt được, mặc dù với sự trợ giúp của các mẫu và xác nhận tĩnh:

template <typename Integer, typename Real>
FooBar MakeFooBar(Integer foo, Real bar) {
  static_assert(std::is_same<Integer, int>::value, "foo should be of type int");
  static_assert(std::is_same<Real, float>::value, "bar should be of type float");
  return { foo, bar };
}

Trong C, bạn có thể đặt tên cho các tham số, nhưng bạn sẽ không bao giờ nhận được thêm.

Mặt khác, nếu tất cả những gì bạn muốn được đặt tên là tham số, thì bạn viết rất nhiều mã rườm rà:

struct FooBarMaker {
  FooBarMaker(int f): _f(f) {}
  FooBar Bar(float b) const { return FooBar(_f, b); }
  int _f;
};

static FooBarMaker Foo(int f) { return FooBarMaker(f); }

// Usage
FooBar fb = Foo(5).Bar(3.4);

Và bạn có thể tiêu trong loại bảo vệ khuyến mãi nếu bạn muốn.


1
"Trong C ++, những gì bạn muốn là có thể đạt được": OP đã yêu cầu giúp ngăn chặn sự trộn lẫn của thứ tự các tham số? Làm thế nào để mẫu bạn đề xuất sẽ đạt được điều đó? Để đơn giản, giả sử chúng ta có 2 tham số, cả hai đều là int.
tối đa

@max: Nó sẽ chỉ ngăn chặn nếu các loại khác nhau (ngay cả khi chúng có thể chuyển đổi sang nhau), đó là tình huống OP. Nếu nó không thể phân biệt các loại, thì tất nhiên nó không hoạt động, nhưng đó là một câu hỏi hoàn toàn khác.
Matthieu M.

À hiểu rồi Vâng, đây là hai vấn đề khác nhau và tôi đoán rằng vấn đề thứ hai không có giải pháp tốt trong C ++ vào lúc này (nhưng có vẻ như C ++ 20 đang bổ sung hỗ trợ cho các tên tham số kiểu C99 trong khởi tạo tổng hợp).
tối đa

6

Nhiều giao diện C ++ của trình biên dịch (bao gồm GCC và clang) hiểu cú pháp khởi tạo C. Nếu bạn có thể, chỉ cần sử dụng phương pháp đó.


16
Mà không tuân thủ tiêu chuẩn C ++!
bitmask

5
Tôi biết nó không chuẩn. Nhưng nếu bạn có thể sử dụng nó, đó vẫn là cách hợp lý nhất để khởi tạo một cấu trúc.
Matthias Urlichs

2
Bạn có thể bảo vệ các loại x và y khiến nhà xây dựng sai thành riêng tư:private: FooBar(float x, int y) {};
dmitry_romanov 18/07/13

4
clang (trình biên dịch c ++ dựa trên llvm) cũng hỗ trợ cú pháp này. Quá tệ, nó không phải là một phần của tiêu chuẩn.
nimrodm

Chúng ta đều biết rằng các trình khởi tạo C không phải là một phần của tiêu chuẩn C ++. Nhưng nhiều trình biên dịch hiểu nó và câu hỏi không cho biết trình biên dịch nào đang được nhắm mục tiêu, nếu có. Vì vậy, xin vui lòng không downvote câu trả lời này.
Matthias Urlichs

4

Một cách khác trong C ++ là

struct Point
{
private:

 int x;
 int y;

public:
    Point& setX(int xIn) { x = Xin; return *this;}
    Point& setY(int yIn) { y = Yin; return *this;}

}

Point pt;
pt.setX(20).setY(20);

2
Cồng kềnh cho lập trình chức năng (tức là tạo đối tượng trong danh sách đối số của lệnh gọi hàm), nhưng thực sự là một ý tưởng gọn gàng!
bitmask

27
Trình tối ưu hóa có thể làm giảm nó, nhưng mắt tôi thì không.
Matthieu M.

6
Hai từ: argh ... argh! Làm thế nào là tốt hơn so với việc sử dụng dữ liệu công cộng với 'Điểm pt; pt.x = pt.y = 20; `? Hoặc nếu bạn muốn đóng gói, làm thế nào điều này tốt hơn một hàm tạo?
OldPeculier

3
Nó tốt hơn một hàm tạo vì bạn phải xem khai báo hàm tạo cho thứ tự tham số ... là x, y hoặc y, x nhưng cách tôi đã chỉ ra là rõ ràng tại trang web cuộc gọi
parapura rajkumar

2
Điều này không hoạt động nếu bạn muốn một cấu trúc const. hoặc nếu bạn muốn nói với trình biên dịch không cho phép các cấu trúc chưa được khởi tạo. Nếu bạn thực sự muốn làm theo cách này, ít nhất hãy đánh dấu các setters với inline!
Matthias Urlichs

3

Tùy chọn D:

FooBar FooBarMake(int foo, float bar)

Pháp lý C, pháp lý C ++. Dễ dàng tối ưu hóa cho PODs. Tất nhiên không có đối số được đặt tên, nhưng điều này giống như tất cả C ++. Nếu bạn muốn các đối số được đặt tên, Objective C nên là lựa chọn tốt hơn.

Tùy chọn E:

FooBar fb;
memset(&fb, 0, sizeof(FooBar));
fb.foo = 4;
fb.bar = 15.5f;

Pháp lý C, pháp lý C ++. Lập luận được đặt tên.


12
Thay vì bộ nhớ bạn có thể sử dụng FooBar fb = {};trong C ++, nó mặc định khởi tạo tất cả các thành viên cấu trúc.
Öö Tiib

@ ÖTiib: Thật không may, mặc dù đó là C bất hợp pháp.
CB Bailey

3

Tôi biết câu hỏi này đã cũ, nhưng có một cách để giải quyết vấn đề này cho đến khi C ++ 20 cuối cùng đưa tính năng này từ C sang C ++. Những gì bạn có thể làm để giải quyết vấn đề này là sử dụng các macro tiền xử lý với static_asserts để kiểm tra việc khởi tạo của bạn có hợp lệ không. (Tôi biết macro nói chung là xấu, nhưng ở đây tôi không thấy một cách khác.) Xem mã ví dụ bên dưới:

#define INVALID_STRUCT_ERROR "Instantiation of struct failed: Type, order or number of attributes is wrong."

#define CREATE_STRUCT_1(type, identifier, m_1, p_1) \
{ p_1 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_2(type, identifier, m_1, p_1, m_2, p_2) \
{ p_1, p_2 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_4(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3, m_4, p_4) \
{ p_1, p_2, p_3, p_4 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_4) >= (offsetof(type, m_3) + sizeof(identifier.m_3)), INVALID_STRUCT_ERROR);\

// Create more macros for structs with more attributes...

Sau đó, khi bạn có một cấu trúc với các thuộc tính const, bạn có thể làm điều này:

struct MyStruct
{
    const int attr1;
    const float attr2;
    const double attr3;
};

const MyStruct test = CREATE_STRUCT_3(MyStruct, test, attr1, 1, attr2, 2.f, attr3, 3.);

Điều này hơi bất tiện, vì bạn cần macro cho mọi số lượng thuộc tính có thể và bạn cần lặp lại loại và tên của thể hiện của bạn trong lệnh gọi macro. Ngoài ra, bạn không thể sử dụng macro trong câu lệnh return, bởi vì các xác nhận xuất hiện sau khi khởi tạo.

Nhưng nó giải quyết vấn đề của bạn: Khi bạn thay đổi cấu trúc, cuộc gọi sẽ thất bại tại thời gian biên dịch.

Nếu bạn sử dụng C ++ 17, bạn thậm chí có thể làm cho các macro này nghiêm ngặt hơn bằng cách buộc các loại tương tự, ví dụ:

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_1) == typeid(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_2) == typeid(identifier.m_2), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_3) == typeid(identifier.m_3), INVALID_STRUCT_ERROR);\

Có một đề xuất C ++ 20 để cho phép các trình khởi tạo được đặt tên không?
Maël Nison


2

Cách /* B */ tốt là trong C ++, C ++ 0x sẽ mở rộng cú pháp nên nó cũng hữu ích cho các thùng chứa C ++. Tôi không hiểu tại sao bạn gọi nó là phong cách xấu?

Nếu bạn muốn chỉ ra các tham số có tên thì bạn có thể sử dụng thư viện tham số boost , nhưng nó có thể gây nhầm lẫn cho ai đó không quen thuộc với nó.

Sắp xếp lại các thành viên cấu trúc giống như sắp xếp lại các tham số hàm, việc tái cấu trúc như vậy có thể gây ra vấn đề nếu bạn không thực hiện cẩn thận.


7
Tôi gọi nó là phong cách xấu bởi vì tôi nghĩ rằng nó là không thể duy trì. Nếu tôi thêm một thành viên khác trong một năm thì sao? Hoặc nếu tôi thay đổi thứ tự / loại của các thành viên? Mỗi đoạn mã khởi tạo nó có thể (rất có thể) bị phá vỡ.
bitmask

2
@bitmask Nhưng miễn là bạn không có tên đối số, bạn cũng sẽ phải cập nhật các cuộc gọi của nhà xây dựng và tôi nghĩ không nhiều người nghĩ rằng các nhà xây dựng là phong cách xấu không thể nhầm lẫn. Tôi cũng nghĩ khởi tạo có tên không phải là C, mà là C99, trong đó C ++ chắc chắn không phải là siêu bộ.
Christian Rau

2
Nếu bạn thêm một thành viên khác trong một năm vào cuối cấu trúc thì nó sẽ được khởi tạo mặc định trong mã đã tồn tại. Nếu bạn sắp xếp lại chúng thì bạn phải chỉnh sửa tất cả các mã hiện có, không có gì để làm.
Öö Tiib

1
@bitmask: Ví dụ đầu tiên cũng sẽ là "không thể nhầm lẫn". Điều gì xảy ra nếu bạn đổi tên một biến trong cấu trúc thay thế? Chắc chắn, bạn có thể thực hiện thay thế tất cả, nhưng điều đó có thể vô tình đổi tên một biến không nên đổi tên.
Mike Bailey

@ChristianRau Từ khi nào C99 không phải C? Không phải nhóm C và C99 là phiên bản cụ thể / thông số kỹ thuật ISO?
altendky

1

Cú pháp này thì sao?

typedef struct
{
    int a;
    short b;
}
ABCD;

ABCD abc = { abc.a = 5, abc.b = 7 };

Chỉ cần thử nghiệm trên Microsoft Visual C ++ 2015 và trên g ++ 6.0.2. Làm việc tốt.
Bạn cũng có thể tạo một macro cụ thể nếu bạn muốn tránh trùng lặp tên biến.


clang++3.5.0-10 -Weverything -std=c++1zdường như xác nhận điều đó. Nhưng nó không đúng. Bạn có biết nơi tiêu chuẩn xác nhận rằng đây là C ++ hợp lệ không?
bitmask

Tôi không biết, nhưng tôi đã sử dụng nó trong các trình biên dịch khác nhau từ lâu và không thấy bất kỳ vấn đề nào. Bây giờ đã thử nghiệm trên g ++ 4.4.7 - hoạt động tốt.
cls

5
Tôi không nghĩ rằng công việc này. Hãy thử ABCD abc = { abc.b = 7, abc.a = 5 };.
raymai97

@deselect, nó hoạt động vì trường được khởi tạo với giá trị, được trả về bởi toán tử =. Vì vậy, thực sự bạn khởi tạo thành viên lớp hai lần.
Dmytro Ovdiienko

1

Đối với tôi cách lười nhất để cho phép inizialization nội tuyến là sử dụng macro này.

#define METHOD_MEMBER(TYPE, NAME, CLASS) \
CLASS &set_ ## NAME(const TYPE &_val) { NAME = _val; return *this; } \
TYPE NAME;

struct foo {
    METHOD_MEMBER(string, attr1, foo)
    METHOD_MEMBER(int, attr2, foo)
    METHOD_MEMBER(double, attr3, foo)
};

// inline usage
foo test = foo().set_attr1("hi").set_attr2(22).set_attr3(3.14);

Đó là thuộc tính tạo macro và phương pháp tự tham chiếu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.