ผ่านจำนวนตัวแปรของการขัดแย้งรอบ ๆ


333

สมมติว่าฉันมีฟังก์ชั่น C ซึ่งรับอาร์กิวเมนต์จำนวนตัวแปร: ฉันจะเรียกฟังก์ชั่นอื่นซึ่งคาดว่าจะมีจำนวนตัวแปรที่ขัดแย้งกันจากภายในได้อย่างไรโดยผ่านอาร์กิวเมนต์ทั้งหมดที่มีในฟังก์ชันแรก

ตัวอย่าง:

void format_string(char *fmt, ...);

void debug_print(int dbg_lvl, char *fmt, ...) {
    format_string(fmt, /* how do I pass all the arguments from '...'? */);
    fprintf(stdout, fmt);
 }

4
ตัวอย่างของคุณดูแปลก ๆ สำหรับฉันโดยที่คุณส่ง fmt ไปที่ format_string () และ fprintf () format_string () ควรกลับสตริงใหม่อย่างใด?
Kristopher Johnson

2
ตัวอย่างไม่สมเหตุสมผล มันเป็นเพียงการแสดงโครงร่างของรหัส
Vicent Marti

162
"ควร googled": ฉันไม่เห็นด้วย Google มีเสียงดังมาก (ไม่ชัดเจนมักสับสนข้อมูล) การมีดี (โหวตแล้วตอบรับ) บน stackoverflow ช่วยได้จริงๆ!
Ansgar

71
เพียงชั่งน้ำหนักใน: ฉันมาถึงคำถามนี้จาก google และเพราะมันเป็นสแต็คล้นมีความมั่นใจสูงว่าคำตอบจะเป็นประโยชน์ ดังนั้นขอให้ออกไป!
tenpn

32
@Ilya: หากไม่มีใครเคยเขียนสิ่งที่อยู่นอก Google จะไม่มีข้อมูลที่จะค้นหาใน Google
Erik Kaplun

คำตอบ:


211

ในการส่งผ่านจุดไข่ปลาคุณต้องแปลงเป็น va_list และใช้ va_list นั้นในฟังก์ชันที่สองของคุณ โดยเฉพาะ;

void format_string(char *fmt,va_list argptr, char *formatted_string);


void debug_print(int dbg_lvl, char *fmt, ...) 
{    
 char formatted_string[MAX_FMT_SIZE];

 va_list argptr;
 va_start(argptr,fmt);
 format_string(fmt, argptr, formatted_string);
 va_end(argptr);
 fprintf(stdout, "%s",formatted_string);
}

3
รหัสนี้นำมาจากคำถามและเป็นเพียงภาพประกอบของวิธีการแปลงจุดไข่ปลาแทนที่จะทำงานอะไรก็ได้ ถ้าคุณมองว่ามันformat_stringแทบจะไม่เป็นประโยชน์เช่นกันเพราะมันจะต้องทำการดัดแปลง in-situ เป็น fmt ซึ่งไม่ควรทำอย่างแน่นอน ตัวเลือกจะรวมถึงการกำจัด format_string ทั้งหมดและใช้ vfprintf แต่นั่นทำให้สมมติฐานเกี่ยวกับสิ่งที่ format_string จริงหรือมี format_string กลับสตริงที่แตกต่างกัน ฉันจะแก้ไขคำตอบเพื่อแสดงหลัง
SmacL

1
หากสตริงรูปแบบของคุณเกิดขึ้นเพื่อใช้คำสั่งสตริงรูปแบบเดียวกันกับ printf คุณสามารถรับคอมไพเลอร์บางอย่างเช่น gcc และเสียงดังกราวเพื่อให้คำเตือนหากสตริงรูปแบบของคุณไม่เข้ากันกับอาร์กิวเมนต์ที่ส่งผ่านจริงดูแอตทริบิวต์ของ GCC ' รูปแบบ' สำหรับรายละเอียดเพิ่มเติม: gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html
Doug Richardson

1
ดูเหมือนจะไม่ทำงานหากคุณผ่าน args สองครั้งติดต่อกัน
fotanus

2
@fananus: ถ้าคุณเรียกใช้ฟังก์ชั่นที่มีargptrและฟังก์ชั่นที่เรียกว่าใช้argptrเลยสิ่งเดียวที่ปลอดภัยที่ต้องทำคือโทรออกva_end()แล้วรีสตาร์ทva_start(argptr, fmt);เพื่อเริ่มต้นใหม่ หรือคุณสามารถใช้va_copy()หากระบบของคุณรองรับ (C99 และ C11 ต้องการ; C89 / 90 ไม่ได้)
Jonathan Leffler

1
โปรดทราบว่าความคิดเห็นของ @ ThomasPadron-McCarthy ล้าสมัยแล้วและ fprintf สุดท้ายนั้นใช้ได้
เฟรดเดอริก

59

ไม่มีวิธีใดในการโทร (เช่น) printf โดยไม่ทราบว่าคุณมีข้อโต้แย้งกี่ข้อที่คุณส่งผ่านเว้นแต่ว่าคุณต้องการใช้กลอุบายที่ซุกซนและไม่พกพาได้

วิธีการแก้ปัญหาที่ใช้โดยทั่วไปคือการมักจะให้รูปแบบอื่นของการทำงาน vararg เพื่อให้printfมีvprintfซึ่งจะใช้เวลาในสถานที่ของva_list รุ่นนี้เป็นเพียงห่อรอบรุ่น......va_list


โปรดทราบว่าไม่vsyslogเป็นไปตามPOSIX
patryk.beza

53

ฟังก์ชั่น Variadicอาจเป็นอันตรายได้ นี่คือเคล็ดลับที่ปลอดภัยยิ่งขึ้น:

   void func(type* values) {
        while(*values) {
            x = *values++;
            /* do whatever with x */
        }
    }

func((type[]){val1,val2,val3,val4,0});

11
เคล็ดลับนี้ดีกว่าคือ: #define callVardicMethodSafely(values...) ({ values *v = { values }; _actualFunction(values, sizeof(v) / sizeof(*v)); })
Richard J. Ross III

5
@ RichardJ.RossIII ฉันหวังว่าคุณจะขยายความคิดเห็นของคุณมันแทบจะไม่สามารถอ่านได้เช่นนี้ฉันไม่สามารถหาแนวคิดที่อยู่เบื้องหลังรหัสและจริง ๆ แล้วมันดูน่าสนใจและมีประโยชน์มาก
Penelope

5
@ArtOfWarfare ฉันไม่แน่ใจว่าฉันยอมรับว่ามันเป็นแฮ็คที่ไม่ดี Rose มีวิธีแก้ปัญหาที่ดี แต่มันเกี่ยวกับการพิมพ์ func ((type []) {val1, val2, 0}); ซึ่งรู้สึก clunky ถ้าคุณมี #define func_short_cut (... ) func ((ประเภท []) { VA_ARGS }); จากนั้นคุณสามารถเรียก func_short_cut (1, 2, 3, 4, 0); ซึ่งให้ไวยากรณ์แบบเดียวกับฟังก์ชัน Variadic ปกติพร้อมกับประโยชน์เพิ่มเติมของเคล็ดลับที่ประณีตของ Rose ... มันเป็นปัญหาอะไรที่นี่?
chrispepper1989

9
ถ้าคุณต้องการส่ง 0 เป็นอาร์กิวเมนต์
Julian Gold

1
สิ่งนี้กำหนดให้ผู้ใช้ของคุณต้องจำไว้ว่าต้องโทรออกด้วยการยกเลิก 0 มันปลอดภัยกว่าได้อย่างไร?
cp.engr

29

ใน C ++ 0x ที่งดงามคุณสามารถใช้เทมเพลตแบบแปรผันได้:

template <typename ... Ts>
void format_string(char *fmt, Ts ... ts) {}

template <typename ... Ts>
void debug_print(int dbg_lvl, char *fmt, Ts ... ts)
{
  format_string(fmt, ts...);
}

อย่าลืมว่าแม่แบบแปรปรวนยังคงไม่พร้อมใช้งานใน Visual Studio ... นี่อาจไม่เกี่ยวข้องกับคุณแน่นอน!
Tom Swirly

1
หากคุณใช้ Visual Studio สามารถเพิ่มเทมเพลต Variadic ใน Visual Studio 2012 โดยใช้ CTP เดือนพฤศจิกายน 2555 หากคุณใช้ Visual Studio 2013 คุณจะมีเทมเพลตหลากหลาย
user2023370

7

คุณสามารถใช้ชุดประกอบแบบอินไลน์สำหรับการเรียกใช้ฟังก์ชัน (ในรหัสนี้ฉันถือว่าอาร์กิวเมนต์เป็นตัวอักษร)

void format_string(char *fmt, ...);
void debug_print(int dbg_level, int numOfArgs, char *fmt, ...)
    {
        va_list argumentsToPass;
        va_start(argumentsToPass, fmt);
        char *list = new char[numOfArgs];
        for(int n = 0; n < numOfArgs; n++)
            list[n] = va_arg(argumentsToPass, char);
        va_end(argumentsToPass);
        for(int n = numOfArgs - 1; n >= 0; n--)
        {
            char next;
            next = list[n];
            __asm push next;
        }
        __asm push fmt;
        __asm call format_string;
        fprintf(stdout, fmt);
    }

4
ไม่ใช่แบบพกพาขึ้นอยู่กับคอมไพเลอร์และป้องกันการปรับให้เหมาะสมของคอมไพเลอร์ ทางออกที่เลวร้ายมาก
Geoffroy

4
ใหม่โดยไม่ลบเช่นกัน
user7116

8
อย่างน้อยสิ่งนี้จริงตอบคำถามโดยไม่ต้องกำหนดคำถามใหม่
lama12345

6

คุณสามารถลองใช้มาโครได้เช่นกัน

#define NONE    0x00
#define DBG     0x1F
#define INFO    0x0F
#define ERR     0x07
#define EMR     0x03
#define CRIT    0x01

#define DEBUG_LEVEL ERR

#define WHERESTR "[FILE : %s, FUNC : %s, LINE : %d]: "
#define WHEREARG __FILE__,__func__,__LINE__
#define DEBUG(...)  fprintf(stderr, __VA_ARGS__)
#define DEBUG_PRINT(X, _fmt, ...)  if((DEBUG_LEVEL & X) == X) \
                                      DEBUG(WHERESTR _fmt, WHEREARG,__VA_ARGS__)

int main()
{
    int x=10;
    DEBUG_PRINT(DBG, "i am x %d\n", x);
    return 0;
}

6

แม้ว่าคุณจะสามารถแก้ปัญหาการส่งผ่านฟอร์แมตเตอร์ได้โดยการจัดเก็บไว้ในบัฟเฟอร์ในเครื่องก่อน แต่ก็ต้องใช้สแต็กและบางครั้งก็อาจมีปัญหาในการจัดการ ฉันลองทำตามและดูเหมือนว่าจะทำงานได้ดี

#include <stdarg.h>
#include <stdio.h>

void print(char const* fmt, ...)
{
    va_list arg;
    va_start(arg, fmt);
    vprintf(fmt, arg);
    va_end(arg);
}

void printFormatted(char const* fmt, va_list arg)
{
    vprintf(fmt, arg);
}

void showLog(int mdl, char const* type, ...)
{
    print("\nMDL: %d, TYPE: %s", mdl, type);

    va_list arg;
    va_start(arg, type);
    char const* fmt = va_arg(arg, char const*);
    printFormatted(fmt, arg);
    va_end(arg);
}

int main() 
{
    int x = 3, y = 6;
    showLog(1, "INF, ", "Value = %d, %d Looks Good! %s", x, y, "Infact Awesome!!");
    showLog(1, "ERR");
}

หวังว่านี่จะช่วยได้


2

น้ำยาของ Ross ทำความสะอาดนิดหน่อย ใช้งานได้หาก args ทั้งหมดเป็นตัวชี้เท่านั้น การใช้ภาษาจะต้องสนับสนุนการลบเครื่องหมายจุลภาคก่อนหน้าถ้า__VA_ARGS__ว่างเปล่า (ทั้ง Visual Studio C ++ และ GCC ทำ)

// pass number of arguments version
 #define callVardicMethodSafely(...) {value_t *args[] = {NULL, __VA_ARGS__}; _actualFunction(args+1,sizeof(args) / sizeof(*args) - 1);}


// NULL terminated array version
 #define callVardicMethodSafely(...) {value_t *args[] = {NULL, __VA_ARGS__, NULL}; _actualFunction(args+1);}

0

สมมติว่าคุณมีฟังก์ชัน Variadic ทั่วไปที่คุณเขียน เนื่องจากต้องมีอาร์กิวเมนต์อย่างน้อยหนึ่งตัวก่อนที่จะมีอาร์กิวเมนต์แบบแปรผัน...คุณจะต้องเขียนอาร์กิวเมนต์เพิ่มเติมในการใช้งานเสมอ

หรือคุณ

หากคุณหุ้มฟังก์ชั่น Variadic ของคุณในแมโครคุณไม่จำเป็นต้องมี arg ก่อนหน้า ลองพิจารณาตัวอย่างนี้:

#define LOGI(...)
    ((void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__))

เห็นได้ชัดว่าสะดวกกว่าเนื่องจากคุณไม่จำเป็นต้องระบุอาร์กิวเมนต์เริ่มต้นทุกครั้ง


-5

ฉันไม่แน่ใจว่าจะใช้งานได้กับคอมไพเลอร์ทั้งหมดหรือไม่ แต่มันได้ผลสำหรับฉันแล้ว

void inner_func(int &i)
{
  va_list vars;
  va_start(vars, i);
  int j = va_arg(vars);
  va_end(vars); // Generally useless, but should be included.
}

void func(int i, ...)
{
  inner_func(i);
}

คุณสามารถเพิ่ม ... ลงใน inner_func () ได้ถ้าต้องการ แต่ไม่ต้องการ มันทำงานได้เพราะ va_start ใช้ที่อยู่ของตัวแปรที่กำหนดเป็นจุดเริ่มต้น ในกรณีนี้เราจะให้การอ้างอิงกับตัวแปรใน func () ดังนั้นจึงใช้ที่อยู่นั้นและอ่านตัวแปรหลังจากนั้นบนสแต็ก ฟังก์ชัน inner_func () กำลังอ่านจากที่อยู่สแต็คของ func () ดังนั้นจะใช้งานได้ถ้าทั้งสองฟังก์ชั่นใช้กลุ่มสแต็กเดียวกัน

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

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