Mã trùng lặp bằng c ++ 11


80

Tôi hiện đang làm việc trong một dự án và tôi gặp sự cố sau.

Tôi có một phương thức C ++ mà tôi muốn làm việc theo hai cách khác nhau:

void MyFunction()
{
  foo();
  bar();
  foobar();
}

void MyFunctionWithABonus()
{
  foo();
  bar();
  doBonusStuff();
  foobar();
}

Và tôi không muốn sao chép mã của mình vì chức năng thực tế dài hơn nhiều. Vấn đề là tôi không được thêm thời gian thực thi vào chương trình trong bất kỳ trường hợp nào khi MyFunction được gọi thay vì MyFunctionWithABonus. Đó là lý do tại sao tôi không thể chỉ có một tham số boolean mà tôi kiểm tra bằng so sánh C ++.

Ý tưởng của tôi là sử dụng các mẫu C ++ để sao chép hầu như mã của mình, nhưng tôi không thể nghĩ ra cách làm mà tôi không có thêm thời gian thực thi và không phải sao chép mã.

Tôi không phải là chuyên gia về các mẫu nên tôi có thể thiếu một thứ gì đó.

Có ai trong số bạn có một ý tưởng? Hay đó chỉ là điều không thể trong C ++ 11?


64
Tôi có thể hỏi tại sao bạn không thể chỉ cần thêm một kiểm tra boolean? Nếu có nhiều mã trong đó, chi phí của việc kiểm tra boolean đơn giản sẽ không thể bỏ qua.
Joris

39
@plougue Dự đoán nhánh ngày nay rất tốt, đến mức kiểm tra boolean thường mất 0 chu kỳ bộ xử lý để thực thi.
Dan

4
Đồng ý với @Dan. Dự đoán chi nhánh hầu như không có chi phí cao trong những ngày này, đặc biệt nếu bạn đang vào một chi nhánh cụ thể nhiều lần.
Akshay Arora

6
@Dan: Một so sánh và chi nhánh tốt nhất vẫn là một uop hợp nhất macro (trên các CPU Intel và AMD x86 hiện đại ), chứ không phải bằng không. Tùy thuộc vào nút thắt cổ chai trong mã của bạn, việc giải mã / phát hành / thực hiện uop này có thể lấy cắp chu kỳ từ thứ khác, giống như cách mà một lệnh ADD bổ sung có thể làm được. Ngoài ra, chỉ cần truyền tham số boolean và nó buộc một thanh ghi (hoặc phải được đổ / tải lại) là một số lệnh khác không. Hy vọng rằng chức năng này inlines nên cuộc gọi và arg-đi trên cao là không có mọi thời gian, và có thể CMP + chi nhánh, nhưng vẫn
Peter Cordes

15
Bạn đã viết mã ở định dạng dễ bảo trì trước chưa? Sau đó, hồ sơ của bạn có nói rằng chi nhánh là nút cổ chai? Bạn có dữ liệu để đề xuất thời gian bạn dành cho quyết định nhỏ này là cách sử dụng thời gian tốt nhất không?
GManNickG

Câu trả lời:


55

Với mẫu và lambda, bạn có thể làm:

template <typename F>
void common(F f)
{
  foo();
  bar();
  f();
  foobar();
}

void MyFunction()
{
    common([](){});
}

void MyFunctionWithABonus()
{
  common(&doBonusStuff);
}

hoặc nếu không bạn chỉ có thể tạo prefixsuffixhoạt động.

void prefix()
{
  foo();
  bar();
}

void suffix()
{
    foobar();
}

void MyFunction()
{
    prefix();
    suffix();
}

void MyFunctionWithABonus()
{
    prefix();
    doBonusStuff();
    suffix();
}

12
Tôi thực sự thích hai giải pháp này hơn một tham số boolean (mẫu hoặc cách khác), bất kể lợi thế về thời gian thực thi. Tôi không thích tham số boolean.
Chris Drew

2
Theo hiểu biết của tôi, giải pháp thứ hai sẽ có thêm thời gian chạy do lệnh gọi hàm bổ sung. Đây có phải là trường hợp đầu tiên không? Tôi không chắc chắn như thế nào lambdas làm việc trong trường hợp đó
plougue

10
Nếu các định nghĩa được hiển thị, trình biên dịch có thể sẽ mã nội tuyến và tạo mã giống như mã được tạo cho mã gốc của bạn.
Jarod42

1
@Yakk Tôi nghĩ nó sẽ phụ thuộc vào trường hợp sử dụng cụ thể và "công cụ thưởng" là của ai. Thông thường, tôi thấy việc có các tham số bool, ifs và các thứ bổ sung trong thuật toán chính khiến nó khó đọc hơn và muốn nó "không còn tồn tại" và được đóng gói và đưa vào từ nơi khác. Nhưng tôi nghĩ câu hỏi về thời điểm thích hợp để sử dụng Mô hình Chiến lược có lẽ nằm ngoài phạm vi của câu hỏi này.
Chris Drew

2
Tối ưu hóa cuộc gọi đuôi thường quan trọng khi bạn muốn tối ưu hóa các trường hợp đệ quy. Trong trường hợp này, nội tuyến đơn giản ... thực hiện mọi thứ bạn cần.
Yakk - Adam Nevraumont

128

Một cái gì đó như vậy sẽ làm tốt:

template<bool bonus = false>
void MyFunction()
{
  foo();
  bar();
  if (bonus) { doBonusStuff(); }
  foobar();
}

Gọi nó qua:

MyFunction<true>();
MyFunction<false>();
MyFunction(); // Call myFunction with the false template by default

Tất cả các mẫu "xấu xí" có thể được tránh bằng cách thêm một số trình bao bọc đẹp vào các chức năng:

void MyFunctionAlone() { MyFunction<false>(); }
void MyFunctionBonus() { MyFunction<true>(); }

Bạn có thể tìm thấy một số thông tin tốt về kỹ thuật đó ở đó . Đó là một bài báo "cũ", nhưng kỹ thuật tự nó vẫn hoàn toàn đúng.

Với điều kiện bạn có quyền truy cập vào một trình biên dịch C ++ 17 đẹp, bạn thậm chí có thể đẩy mạnh kỹ thuật này hơn nữa, bằng cách sử dụng constexpr nếu , như thế:

template <int bonus>
auto MyFunction() {
  foo();
  bar();
  if      constexpr (bonus == 0) { doBonusStuff1(); }
  else if constexpr (bonus == 1) { doBonusStuff2(); }
  else if constexpr (bonus == 2) { doBonusStuff3(); }
  else if constexpr (bonus == 3) { doBonusStuff4(); }
  // Guarantee that this function will not compile
  // if a bonus different than 0,1,2,3 is passer
  else { static_assert(false);}, 
  foorbar();
}

11
Và séc đó sẽ được trình biên dịch tối ưu hóa một cách độc đáo
Jonas

21
trong C ++ 17 if constexpr (bonus) { doBonusStuff(); } .
Chris Drew

5
@ChrisDrew Không chắc chắn với constexpr nếu điều đó sẽ thêm bất cứ điều gì ở đây. Nó sẽ?
Gibet

13
@Gibet: Nếu lệnh gọi đến doBonusStuff()thậm chí không thể biên dịch vì một số lý do trong trường hợp không phải bonus, nó sẽ tạo ra sự khác biệt rất lớn.
Lightness Races in Orbit

4
@WorldSEnder Có, bạn có thể, nếu theo enums hoặc enum, bạn có nghĩa là constexpr (bonus == MyBonus :: ExtraSpeed).
Gibet

27

Với một số nhận xét mà OP đã đưa ra liên quan đến việc gỡ lỗi, đây là phiên bản yêu doBonusStuff()cầu các bản dựng gỡ lỗi, nhưng không phát hành các bản dựng (định nghĩa NDEBUG):

#if defined(NDEBUG)
#define DEBUG(x)
#else
#define DEBUG(x) x
#endif

void MyFunctionWithABonus()
{
  foo();
  bar();
  DEBUG(doBonusStuff());
  foobar();
}

Bạn cũng có thể sử dụng assertmacro nếu bạn muốn kiểm tra một điều kiện và thất bại nếu nó sai (nhưng chỉ dành cho các bản dựng gỡ lỗi; các bản dựng phát hành sẽ không thực hiện kiểm tra).

Hãy cẩn thận nếu doBonusStuff()có tác dụng phụ, vì những tác dụng phụ này sẽ không có trong các bản phát hành và có thể làm mất hiệu lực của các giả định được đưa ra trong mã.


Cảnh báo về tác dụng phụ là tốt, nhưng nó cũng đúng bất kể cấu trúc nào được sử dụng, có thể là mẫu, if () {...}, constexpr, v.v.
pipe

Với các ý kiến ​​của OP, bản thân tôi đã ủng hộ điều này vì nó chính xác là giải pháp tốt nhất cho họ. Điều đó nói rằng, chỉ là một sự tò mò: tại sao tất cả các biến chứng với các định nghĩa mới và mọi thứ, khi bạn chỉ có thể đặt lệnh gọi doBonusStuff () bên trong một #if được định nghĩa (NDEBUG) ??
motoDrizzt

@motoDrizzt: Nếu OP muốn làm điều này tương tự trong các chức năng khác, tôi sẽ giới thiệu một macro mới như macro này sạch hơn / dễ đọc (và ghi) hơn. Nếu đó chỉ là việc một lần, thì tôi đồng ý rằng chỉ cần sử dụng #if defined(NDEBUG)trực tiếp có lẽ sẽ dễ dàng hơn.
Cornstalks

@Cornstalks vâng, nó hoàn toàn có ý nghĩa, tôi đã không nghĩ đến điều đó. Và tôi vẫn đang nghĩ đến điều này cần được trả lời chấp nhận :-)
motoDrizzt

18

Đây là một biến thể nhỏ về câu trả lời của Jarod42 bằng cách sử dụng các mẫu khác nhau để người gọi có thể cung cấp không hoặc một chức năng bổ sung:

void callBonus() {}

template<typename F>
void callBonus(F&& f) { f(); }

template <typename ...F>
void MyFunction(F&&... f)
{
  foo();
  bar();
  callBonus(std::forward<F>(f)...);
  foobar();
}

Mã gọi:

MyFunction();
MyFunction(&doBonusStuff);

11

Một phiên bản khác, chỉ sử dụng các mẫu và không có chức năng chuyển hướng, vì bạn đã nói rằng bạn không muốn bất kỳ chi phí thời gian chạy nào. Như tôi lo ngại, điều này chỉ làm tăng thời gian biên dịch:

#include <iostream>

using namespace std;

void foo() { cout << "foo\n"; };
void bar() { cout << "bar\n"; };
void bak() { cout << "bak\n"; };

template <bool = false>
void bonus() {};

template <>
void bonus<true>()
{
    cout << "Doing bonus\n";
};

template <bool withBonus = false>
void MyFunc()
{
    foo();
    bar();
    bonus<withBonus>();
    bak();
}

int main(int argc, const char* argv[])
{
    MyFunc();
    cout << "\n";
    MyFunc<true>();
}

output:
foo
bar
bak

foo
bar
Doing bonus
bak

Bây giờ chỉ có một phiên bản MyFunc()với booltham số làm đối số mẫu.


Nó không thêm thời gian biên dịch bằng cách gọi bonus ()? Hay trình biên dịch phát hiện ra rằng bonus <false> trống và không chạy lệnh gọi hàm?
plougue

1
bonus<false>()gọi phiên bản mặc định của bonusmẫu (dòng 9 và 10 của ví dụ), vì vậy không có lệnh gọi hàm. Nói cách khác, MyFunc()biên dịch thành một khối mã (không có điều kiện trong đó) và MyFunc<true>()biên dịch sang một khối mã khác (không có điều kiện trong đó).
David K

6
Các mẫu @plougue là nội tuyến hoàn toàn và các hàm trống nội tuyến không làm được gì và có thể bị trình biên dịch loại bỏ.
Yakk - Adam Nevraumont

8

Bạn có thể sử dụng điều phối thẻ và quá tải chức năng đơn giản:

struct Tag_EnableBonus {};
struct Tag_DisableBonus {};

void doBonusStuff(Tag_DisableBonus) {}

void doBonusStuff(Tag_EnableBonus)
{
    //Do bonus stuff here
}

template<class Tag> MyFunction(Tag bonus_tag)
{
   foo();
   bar();
   doBonusStuff(bonus_tag);
   foobar();
}

Điều này rất dễ đọc / dễ hiểu, có thể được mở rộng mà không tốn nhiều công sức (và không có ifmệnh đề viết sẵn - bằng cách thêm nhiều thẻ hơn) và tất nhiên sẽ không để lại dấu vết thời gian chạy.

Cú pháp gọi của nó khá thân thiện, nhưng tất nhiên có thể được gói gọn trong các cuộc gọi vani:

void MyFunctionAlone() { MyFunction(Tag_DisableBonus{}); }
void MyFunctionBonus() { MyFunction(Tag_EnableBonus{}); }

Điều phối thẻ là một kỹ thuật lập trình chung được sử dụng rộng rãi, đây là một bài viết hay về những điều cơ bản.

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.