C ++ Phương pháp ưa thích để xử lý triển khai cho các mẫu lớn


10

Thông thường khi khai báo một lớp C ++, cách tốt nhất là chỉ đặt khai báo trong tệp tiêu đề và đặt việc thực hiện trong một tệp nguồn. Tuy nhiên, dường như mô hình thiết kế này không hoạt động cho các lớp mẫu.

Khi tìm kiếm trực tuyến dường như có 2 ý kiến ​​về cách tốt nhất để quản lý các lớp mẫu:

1. Toàn bộ khai báo và thực hiện trong tiêu đề.

Điều này khá đơn giản nhưng theo tôi, khó duy trì và chỉnh sửa các tệp mã khi mẫu trở nên lớn.

2. Viết việc thực hiện trong một mẫu bao gồm tệp (.tpp) được bao gồm ở cuối.

Đây có vẻ là một giải pháp tốt hơn cho tôi nhưng dường như không được áp dụng rộng rãi. Có một lý do mà cách tiếp cận này là kém hơn?

Tôi biết rằng nhiều lần phong cách mã được quyết định bởi sở thích cá nhân hoặc phong cách kế thừa. Tôi đang bắt đầu một dự án mới (chuyển một dự án C cũ sang C ++) và tôi còn khá mới với thiết kế OO và muốn làm theo các thực tiễn tốt nhất ngay từ đầu.


1
Xem bài viết 9 tuổi này trên codeproject.com. Phương pháp 3 là những gì bạn mô tả. Có vẻ như không đặc biệt như bạn tin.
Doc Brown

.. hoặc ở đây, cùng một phương pháp, bài viết từ năm 2014: codeofhonour.blogspot.com/2014/11/...
Doc Brown

2
Liên quan chặt chẽ: stackoverflow.com/q/1208028/179910 . Gnu thường sử dụng tiện ích mở rộng ".tcc" thay vì ".tpp", nhưng về mặt khác thì nó khá giống nhau.
Jerry Coffin

Tôi luôn sử dụng "ipp" làm phần mở rộng, nhưng tôi đã làm điều tương tự rất nhiều trong mã tôi đã viết.
Sebastian Redl

Câu trả lời:


6

Khi viết một lớp C ++ templated, bạn thường có ba tùy chọn:

(1) Đặt khai báo và định nghĩa trong tiêu đề.

// foo.h
#pragma once

template <typename T>
struct Foo
{
    void f()
    {
        ...
    }
};

hoặc là

// foo.h
#pragma once

template <typename T>
struct Foo
{
    void f();
};

template <typename T>
inline void Foo::f()
{
    ...
}

Chuyên nghiệp:

  • Sử dụng rất thuận tiện (chỉ bao gồm các tiêu đề).

Con:

  • Giao diện và phương thức thực hiện được trộn lẫn. Đây là "chỉ" một vấn đề dễ đọc. Một số tìm thấy điều này không thể nhầm lẫn, bởi vì nó khác với cách tiếp cận .h / .cpp thông thường. Tuy nhiên, lưu ý rằng điều này không có vấn đề trong các ngôn ngữ khác, ví dụ, C # và Java.
  • Tác động xây dựng lại cao: Nếu bạn khai báo một lớp mới với Footư cách là thành viên, bạn cần bao gồm foo.h. Điều này có nghĩa là thay đổi việc thực hiện Foo::flan truyền thông qua cả tệp tiêu đề và tệp nguồn.

Hãy xem xét kỹ hơn về tác động xây dựng lại: Đối với các lớp C ++ không có templated, bạn đặt các khai báo trong .h và các định nghĩa phương thức trong .cpp. Theo cách này, khi việc thực hiện một phương thức được thay đổi, chỉ cần biên dịch lại một .cpp. Điều này khác với các lớp mẫu nếu .h chứa tất cả mã của bạn. Hãy xem ví dụ sau:

// bar.h
#pragma once
#include "foo.h"
struct Bar
{
    void b();
    Foo<int> foo;
};

// bar.cpp
#include "bar.h"
void Bar::b()
{
    foo.f();
}

// qux.h
#pragma once
#include "bar.h"
struct Qux
{
    void q();
    Bar bar;
}

// qux.cpp
#include "qux.h"
void Qux::q()
{
    bar.b();
}

Ở đây, cách sử dụng duy nhất Foo::flà bên trong bar.cpp. Tuy nhiên, nếu bạn thay đổi việc thực hiện Foo::f, cả hai bar.cppqux.cppcần phải được biên dịch lại. Việc thực hiện Foo::fcuộc sống trong cả hai tệp, mặc dù không có phần nào Quxtrực tiếp sử dụng bất cứ thứ gì Foo::f. Đối với các dự án lớn, điều này có thể sớm trở thành một vấn đề.

(2) Đặt khai báo trong .h và định nghĩa trong .tpp và đưa nó vào .h.

// foo.h
#pragma once
template <typename T>
struct Foo
{
    void f();
};
#include "foo.tpp"    

// foo.tpp
#pragma once // not necessary if foo.h is the only one that includes this file
template <typename T>
inline void Foo::f()
{
    ...
}

Chuyên nghiệp:

  • Sử dụng rất thuận tiện (chỉ bao gồm các tiêu đề).
  • Các định nghĩa giao diện và phương thức được tách ra.

Con:

  • Tác động xây dựng lại cao (giống như (1) ).

Giải pháp này phân tách khai báo và định nghĩa phương thức trong hai tệp riêng biệt, giống như .h / .cpp. Tuy nhiên, cách tiếp cận này có cùng một vấn đề xây dựng lại như (1) , bởi vì tiêu đề trực tiếp bao gồm các định nghĩa phương thức.

(3) Đặt khai báo trong .h và định nghĩa trong .tpp, nhưng không bao gồm .tpp trong .h.

// foo.h
#pragma once
template <typename T>
struct Foo
{
    void f();
};

// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
    ...
}

Chuyên nghiệp:

  • Giảm tác động xây dựng lại giống như tách .h / .cpp.
  • Các định nghĩa giao diện và phương thức được tách ra.

Con:

  • Sử dụng không thuận tiện: Khi thêm một Foothành viên vào một lớp Bar, bạn cần đưa foo.hvào tiêu đề. Nếu bạn gọi Foo::fmột .cpp, bạn cũng phải bao gồm foo.tppở đó.

Cách tiếp cận này làm giảm tác động xây dựng lại, vì chỉ các tệp .cpp thực sự sử dụng Foo::fcần phải được biên dịch lại. Tuy nhiên, điều này có giá: Tất cả những tệp cần bao gồm foo.tpp. Lấy ví dụ từ trên và sử dụng phương pháp mới:

// bar.h
#pragma once
#include "foo.h"
struct Bar
{
    void b();
    Foo<int> foo;
};

// bar.cpp
#include "bar.h"
#include "foo.tpp"
void Bar::b()
{
    foo.f();
}

// qux.h
#pragma once
#include "bar.h"
struct Qux
{
    void q();
    Bar bar;
}

// qux.cpp
#include "qux.h"
void Qux::q()
{
    bar.b();
}

Như bạn có thể thấy, sự khác biệt duy nhất là bao gồm bổ sung foo.tpptrong bar.cpp. Điều này là bất tiện và thêm một giây bao gồm cho một lớp tùy thuộc vào việc bạn gọi các phương thức trên nó có vẻ rất xấu. Tuy nhiên, bạn giảm tác động xây dựng lại: Chỉ bar.cppcần được biên dịch lại nếu bạn thay đổi việc thực hiện Foo::f. Các tập tin qux.cppkhông cần biên dịch lại.

Tóm lược:

Nếu bạn triển khai một thư viện, bạn thường không cần quan tâm đến việc xây dựng lại tác động. Người dùng thư viện của bạn lấy một bản phát hành và sử dụng nó và việc triển khai thư viện không thay đổi trong công việc hàng ngày của người dùng. Trong những trường hợp như vậy, thư viện có thể sử dụng cách tiếp cận (1) hoặc (2) và đó chỉ là vấn đề về hương vị mà bạn chọn.

Tuy nhiên, nếu bạn đang làm việc trên một ứng dụng hoặc nếu bạn đang làm việc trên một thư viện nội bộ của công ty, mã sẽ thay đổi thường xuyên. Vì vậy, bạn phải quan tâm về xây dựng lại tác động. Chọn cách tiếp cận (3) có thể là một lựa chọn tốt nếu bạn khiến các nhà phát triển của mình chấp nhận bổ sung.


2

Tương tự như .tppý tưởng (mà tôi chưa bao giờ thấy được sử dụng), chúng tôi đặt hầu hết chức năng nội tuyến vào một -inl.hpptệp được bao gồm ở cuối .hpptệp thông thường .

Như chỉ ra của người khác, điều này giữ cho giao diện có thể đọc được bằng cách di chuyển sự lộn xộn của các triển khai nội tuyến (như các mẫu) trong một tệp khác. Chúng tôi cho phép một số dòng giao diện nhưng cố gắng giới hạn chúng ở các hàm nhỏ, điển hình là dòng đơn.


1

Một pro pro của biến thể thứ 2 là tiêu đề của bạn trông gọn gàng hơn.

Con lừa có thể là bạn có thể kiểm tra lỗi IDE nội tuyến và các ràng buộc của trình gỡ lỗi đã bị hỏng.


2nd cũng yêu cầu rất nhiều dự phòng khai báo tham số mẫu, có thể trở nên rất dài dòng đặc biệt là khi sử dụng sfinae. Và trái ngược với OP, tôi thấy khó đọc thứ 2 hơn, đặc biệt là do bản tóm tắt dự phòng.
Sốt

0

Tôi rất thích cách tiếp cận đưa việc thực hiện vào một tệp riêng biệt và chỉ có tài liệu và khai báo trong tệp tiêu đề.

Có lẽ lý do bạn chưa thấy phương pháp này được sử dụng nhiều trong thực tế, là bạn chưa tìm đúng nơi ;-)

Hoặc - có lẽ bởi vì nó cần thêm một chút nỗ lực trong việc phát triển phần mềm. Nhưng đối với một thư viện lớp, nỗ lực đó thật đáng giá, IMHO và tự trả tiền cho một thư viện dễ sử dụng / đọc hơn nhiều.

Lấy thư viện này làm ví dụ: https://github.com/SophistSolutions/Stroika/

Toàn bộ thư viện được viết theo cách tiếp cận này và nếu bạn xem qua mã, bạn sẽ thấy nó hoạt động tốt như thế nào.

Các tệp tiêu đề dài bằng các tệp thực hiện, nhưng chúng không chứa gì ngoài các khai báo và tài liệu.

So sánh khả năng đọc của Stroika với khả năng thực hiện std c ++ yêu thích của bạn (gcc hoặc libc ++ hoặc msvc). Tất cả đều sử dụng phương pháp triển khai trong tiêu đề nội tuyến, và mặc dù chúng được viết rất tốt, IMHO, không phải là các triển khai có thể đọc được.

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.