C ++ วิธีที่ต้องการในการจัดการกับการใช้งานสำหรับแม่แบบขนาดใหญ่


10

โดยทั่วไปเมื่อประกาศคลาส C ++ เป็นแนวปฏิบัติที่ดีที่สุดที่จะใส่เฉพาะการประกาศในไฟล์ส่วนหัวและวางการประยุกต์ใช้ในไฟล์ต้นฉบับ อย่างไรก็ตามดูเหมือนว่ารูปแบบการออกแบบนี้จะไม่ทำงานกับคลาสเทมเพลต

เมื่อดูทางออนไลน์มีความคิดเห็น 2 ข้อเกี่ยวกับวิธีที่ดีที่สุดในการจัดการคลาสเทมเพลต:

1. การประกาศทั้งหมดและการใช้งานในส่วนหัว

สิ่งนี้ค่อนข้างตรงไปตรงมา แต่นำไปสู่สิ่งที่ฉันคิดว่าเป็นเรื่องยากที่จะรักษาและแก้ไขไฟล์โค้ดเมื่อเทมเพลตมีขนาดใหญ่

2. เขียนการใช้งานในเทมเพลตรวมไฟล์ (.tpp) ที่รวมอยู่ท้าย

ดูเหมือนว่าจะเป็นทางออกที่ดีกว่าสำหรับฉัน แต่ดูเหมือนจะไม่ได้ใช้อย่างกว้างขวาง มีเหตุผลที่วิธีนี้ด้อยกว่าหรือไม่?

ฉันรู้ว่าหลายครั้งที่รูปแบบของรหัสถูกกำหนดโดยการตั้งค่าส่วนตัวหรือรูปแบบดั้งเดิม ฉันกำลังเริ่มโครงการใหม่ (การย้ายโครงการ C เก่าไปยัง C ++) และฉันค่อนข้างใหม่ในการออกแบบ OO และต้องการปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุดตั้งแต่เริ่มต้น


1
ดูบทความเก่า 9 ปีนี้ที่ codeproject.com วิธีที่ 3 คือสิ่งที่คุณอธิบาย ดูเหมือนจะไม่พิเศษอย่างที่คุณเชื่อ
Doc Brown

.. หรือที่นี่แนวทางเดียวกันบทความจาก 2014: codeofhonour.blogspot.com/2014/11/…
Doc Brown

2
ที่เกี่ยวข้องอย่างใกล้ชิดstackoverflow.com/q/1208028/179910 โดยทั่วไปแล้ว Gnu จะใช้ส่วนขยาย ".tcc" แทน ".tpp" แต่ก็เหมือนกันมาก
Jerry Coffin

ฉันมักจะใช้ "ipp" เป็นส่วนขยาย แต่ฉันทำสิ่งเดียวกันกับรหัสที่ฉันเขียน
เซบาสเตียนเรดล

คำตอบ:


6

เมื่อเขียนคลาส C ++ templated คุณมักจะมีสามตัวเลือก:

(1) ใส่การประกาศและคำนิยามในส่วนหัว

// foo.h
#pragma once

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

หรือ

// foo.h
#pragma once

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

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

มือโปร:

  • การใช้งานที่สะดวกมาก (รวมเฉพาะส่วนหัว)

Con:

  • มีการผสมผสานอินเตอร์เฟสและวิธีการใช้งาน นี่เป็นเพียงปัญหาการอ่าน บางคนพบว่าไม่สามารถรักษาได้เพราะมันแตกต่างจากวิธีการ. h / .cpp ปกติ อย่างไรก็ตามโปรดทราบว่านี่ไม่มีปัญหาในภาษาอื่นเช่น C # และ Java
  • สูงสร้างผลกระทบ: ถ้าคุณประกาศคลาสใหม่กับเป็นสมาชิกคุณต้องรวมFoo foo.hซึ่งหมายความว่าการเปลี่ยนการใช้งานของFoo::fการเผยแพร่ผ่านทั้งส่วนหัวและไฟล์ต้นฉบับ

ให้ดูที่การสร้างใหม่อีกครั้ง: สำหรับคลาส C ++ ที่ไม่ใช่เท็มเพลตคุณใส่การประกาศใน. h และนิยามเมธอดใน. cpp วิธีนี้เมื่อมีการเปลี่ยนแปลงวิธีการใช้งานจะต้องมีการคอมไพล์. cpp เพียงอันเดียวเท่านั้น สิ่งนี้แตกต่างกันสำหรับคลาสเทมเพลตหาก. h มีรหัสทั้งหมดของคุณ ลองดูตัวอย่างต่อไปนี้:

// 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();
}

นี่คือการใช้งานเพียงคนเดียวที่อยู่ภายในFoo::f bar.cppอย่างไรก็ตามหากคุณเปลี่ยนการใช้งานFoo::fทั้งสองbar.cppและqux.cppจำเป็นต้องคอมไพล์ใหม่ การดำเนินงานของFoo::fชีวิตในทั้งสองไฟล์แม้ว่าส่วนหนึ่งของการไม่มีโดยตรงใช้อะไรQux Foo::fสำหรับโครงการขนาดใหญ่สิ่งนี้อาจกลายเป็นปัญหาได้ในไม่ช้า

(2) ใส่การประกาศใน. h และคำจำกัดความเป็น. tpp และรวมไว้ใน. 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()
{
    ...
}

มือโปร:

  • การใช้งานที่สะดวกมาก (รวมเฉพาะส่วนหัว)
  • นิยามอินเตอร์เฟสและเมธอดถูกแยกออก

Con:

  • ผลกระทบการสร้างใหม่สูง (เช่นเดียวกับ(1) )

โซลูชันนี้แยกการประกาศและคำนิยามวิธีการในสองไฟล์แยกกันเช่น. h / .cpp อย่างไรก็ตามวิธีการนี้มีปัญหาการสร้างใหม่เช่นเดียวกับ(1)เนื่องจากส่วนหัวมีคำจำกัดความของวิธีการโดยตรง

(3) ใส่การประกาศใน. h และคำจำกัดความเป็น. tpp แต่ไม่รวม. tpp ใน. h

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

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

มือโปร:

  • ลดผลกระทบของการสร้างใหม่เช่นเดียวกับการแยก. h / .cpp
  • นิยามอินเตอร์เฟสและเมธอดถูกแยกออก

Con:

  • การใช้งานไม่สะดวก: เมื่อเพิ่มFooสมาชิกในคลาสBarคุณต้องรวมfoo.hไว้ในส่วนหัว ถ้าคุณโทรFoo::fใน .cpp คุณยังต้องรวมถึงการfoo.tppมี

วิธีการนี้จะช่วยลดผลกระทบของการสร้างใหม่เนื่องจากไฟล์. cpp ที่Foo::fต้องใช้จริงๆจะต้องทำการคอมไพล์ใหม่ แต่นี้มาในราคา: foo.tppไฟล์ทั้งหมดที่จำเป็นต้องมี นำตัวอย่างจากด้านบนและใช้วิธีการใหม่:

// 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();
}

ในขณะที่คุณสามารถมองเห็นความแตกต่างเพียงอย่างเดียวคือเพิ่มเติมรวมถึงของในfoo.tpp bar.cppสิ่งนี้ไม่สะดวกและการเพิ่มการรวมครั้งที่สองสำหรับชั้นเรียนขึ้นอยู่กับว่าคุณเรียกใช้วิธีการที่ดูเหมือนว่าน่าเกลียดมาก อย่างไรก็ตามคุณลดการสร้างผลกระทบ: เฉพาะจะต้องมีการคอมถ้าคุณเปลี่ยนการดำเนินงานของbar.cpp Foo::fไฟล์qux.cppไม่จำเป็นต้องคอมไพล์ใหม่

สรุป:

หากคุณใช้ไลบรารีคุณไม่จำเป็นต้องสนใจเรื่องการสร้างผลกระทบซ้ำ ผู้ใช้ไลบรารีของคุณคว้ารีลีสและใช้งานและการใช้ไลบรารี่จะไม่เปลี่ยนแปลงในการทำงานประจำวันของผู้ใช้ ในกรณีเช่นนี้ห้องสมุดสามารถใช้วิธีการ(1)หรือ(2)และเป็นเรื่องของรสนิยมที่คุณเลือก

อย่างไรก็ตามหากคุณกำลังทำงานกับแอพพลิเคชั่นหรือหากคุณกำลังทำงานกับห้องสมุดภายในของ บริษัท ของคุณรหัสจะเปลี่ยนไปบ่อยครั้ง ดังนั้นคุณต้องใส่ใจกับการสร้างผลกระทบใหม่ การเลือกวิธีการ(3)อาจเป็นตัวเลือกที่ดีหากคุณให้นักพัฒนาของคุณยอมรับข้อเสนอเพิ่มเติม


2

คล้ายกับ.tppความคิด (ซึ่งฉันไม่เคยเห็นมาก่อน) เราใส่ฟังก์ชั่นอินไลน์ส่วนใหญ่ไว้ใน-inl.hppไฟล์ที่รวมอยู่ท้าย.hppไฟล์ปกติ

ตามที่ระบุอื่น ๆ สิ่งนี้จะช่วยให้อินเตอร์เฟซสามารถอ่านได้โดยการย้ายความยุ่งเหยิงของการใช้งานแบบอินไลน์ (เช่นแม่แบบ) ในไฟล์อื่น เราอนุญาตอินไลน์อินเทอร์เฟซบางอย่าง แต่พยายาม จำกัด ให้เล็กฟังก์ชั่นบรรทัดเดียวโดยทั่วไป


1

เหรียญโปรหนึ่งเหรียญสำหรับตัวแปรที่สองคือส่วนหัวของคุณดูเป็นระเบียบมากขึ้น

คอนอาจเป็นคุณอาจมีการตรวจสอบข้อผิดพลาด IDE แบบอินไลน์และการเชื่อมต่อดีบักเกอร์เมามากขึ้น


อันดับที่ 2 ยังต้องการการประกาศซ้ำพารามิเตอร์เทมเพลตจำนวนมากซึ่งสามารถกลายเป็น verbose โดยเฉพาะอย่างยิ่งเมื่อใช้ sfinae และตรงกันข้ามกับ OP ฉันพบว่ายากกว่าที่จะอ่านรหัสเพิ่มเติมที่นั่นโดยเฉพาะเนื่องจากการต้มซ้ำซ้อน
Sopel

0

ฉันชอบวิธีการวางการนำไปใช้ในไฟล์แยกกันอย่างมากและมีเพียงเอกสารประกอบและการประกาศในไฟล์ส่วนหัว

บางทีเหตุผลที่คุณไม่เคยเห็นวิธีการนี้ใช้ในทางปฏิบัติมากคุณไม่ได้ดูในสถานที่ที่เหมาะสม ;-)

หรือ - อาจเป็นเพราะใช้ความพยายามเล็กน้อยในการพัฒนาซอฟต์แวร์ แต่สำหรับห้องสมุดชั้นเรียนความพยายามนั้นคุ้มค่าในขณะที่ IMHO และจ่ายเองในห้องสมุดที่ใช้ / อ่านง่ายขึ้น

ใช้ห้องสมุดนี้ตัวอย่าง: https://github.com/SophistSolutions/Stroika/

ไลบรารีทั้งหมดเขียนด้วยวิธีการนี้และหากคุณดูรหัสคุณจะเห็นว่ามันใช้งานได้ดีเพียงใด

ไฟล์ส่วนหัวนั้นยาวพอ ๆ กับไฟล์นำไปใช้งาน แต่จะเต็มไปด้วยอะไรนอกจากการประกาศและเอกสารประกอบ

เปรียบเทียบความสามารถในการอ่านของ Stroika กับการใช้ std c ++ ที่คุณโปรดปราน (gcc หรือ libc ++ หรือ msvc) ผู้ใช้ทั้งหมดใช้วิธีการใช้งานแบบอินไลน์ส่วนหัวและแม้ว่าพวกเขาจะเขียนได้ดีมาก IMHO ไม่ใช่การใช้งานที่อ่านง่าย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.